Compare commits
479 Commits
Author | SHA1 | Date |
---|---|---|
tsu-gu | a65c0b6efc | |
tsu-gu | a8de1f8467 | |
tsu-gu | a97b6e3dd8 | |
Sarah Jamie Lewis | 8417204a24 | |
Sarah Jamie Lewis | bf05a52d63 | |
Sarah Jamie Lewis | 23ae1ac0bb | |
Dan Ballard | 7bcde5a1fa | |
Sarah Jamie Lewis | 723b0eb04d | |
Dan Ballard | 119e683d3d | |
Sarah Jamie Lewis | 6abf506f18 | |
Sarah Jamie Lewis | fd8790fab6 | |
Sarah Jamie Lewis | 00c385bb91 | |
Dan Ballard | c2f46a0117 | |
Dan Ballard | f7da7b4bb7 | |
Dan Ballard | 448900b48e | |
Dan Ballard | 4a11968567 | |
Dan Ballard | abec0f3ad5 | |
Sarah Jamie Lewis | 28ce08637b | |
Sarah Jamie Lewis | c8bdc56507 | |
Dan Ballard | 4e895723e2 | |
Dan Ballard | 9958c72d5d | |
Dan Ballard | c498a0c86a | |
Sarah Jamie Lewis | 596b65f12d | |
Sarah Jamie Lewis | 16eda0ce8a | |
Dan Ballard | ff332dee9c | |
Dan Ballard | 8eb2e73b10 | |
Sarah Jamie Lewis | 469624c46c | |
Dan Ballard | ffa52f697a | |
Sarah Jamie Lewis | f585122f57 | |
Sarah Jamie Lewis | be26d40176 | |
Sarah Jamie Lewis | 9a67008ece | |
Sarah Jamie Lewis | 27a729d09a | |
Sarah Jamie Lewis | 7ee619f1a6 | |
Dan Ballard | 993f42113e | |
Sarah Jamie Lewis | 6418170b2e | |
erinn | 91f44d631c | |
erinn | 928a201a3b | |
erinn | 007fb02cba | |
erinn | 65b0ecc0c3 | |
Sarah Jamie Lewis | faba13d435 | |
Dan Ballard | 6746abacd7 | |
Dan Ballard | d4546199e4 | |
Dan Ballard | f29e926d28 | |
Sarah Jamie Lewis | ada351f778 | |
Sarah Jamie Lewis | 65a1280b35 | |
Sarah Jamie Lewis | 3b60bf085a | |
Sarah Jamie Lewis | 3784ec04e3 | |
Sarah Jamie Lewis | 17ffe03dba | |
Sarah Jamie Lewis | 059d32718b | |
Sarah Jamie Lewis | f6710484a2 | |
Sarah Jamie Lewis | b0668812e4 | |
Sarah Jamie Lewis | cfe5f29213 | |
Sarah Jamie Lewis | b866124147 | |
Sarah Jamie Lewis | ed4bb99fde | |
Sarah Jamie Lewis | b282ace9c3 | |
Sarah Jamie Lewis | daa0e65070 | |
Sarah Jamie Lewis | 25e1300b2b | |
Kaio Duarte Costa | d4a87cd416 | |
Kaio Duarte Costa | 0455ed15d7 | |
Kaio Duarte Costa | 1f15e8af39 | |
Kaio Duarte Costa | 32b4ad2576 | |
Sarah Jamie Lewis | 08d337401f | |
Sarah Jamie Lewis | 91eca10f12 | |
Sarah Jamie Lewis | 870e7338ae | |
Sarah Jamie Lewis | af4aab3a47 | |
Dan Ballard | 8972d0eef5 | |
Sarah Jamie Lewis | 881cfbd0a3 | |
Sarah Jamie Lewis | c5f684e42e | |
Dan Ballard | 2d89b30881 | |
Sarah Jamie Lewis | e51c30ecc9 | |
Sarah Jamie Lewis | fb4c438e1c | |
Sarah Jamie Lewis | 2eca5058a8 | |
Dan Ballard | 94297ee85f | |
Sarah Jamie Lewis | 3fe732809d | |
Dan Ballard | 52b1f28252 | |
Dan Ballard | 16f413177f | |
Dan Ballard | ea7b307de2 | |
Dan Ballard | 4d4901838e | |
Dan Ballard | cfa4b4f95b | |
Dan Ballard | 2defc7ea2c | |
Sarah Jamie Lewis | cfb32bc84a | |
Dan Ballard | e570f6941b | |
Sarah Jamie Lewis | 37e18d03a1 | |
Sarah Jamie Lewis | 76c925d874 | |
Dan Ballard | 975983be3c | |
Sarah Jamie Lewis | 7edc46743f | |
erinn | 74ab39067e | |
erinn | cac2064731 | |
Dan Ballard | 93284708e0 | |
Sarah Jamie Lewis | 521c0600a2 | |
Dan Ballard | 7f2a8d649d | |
Dan Ballard | d550c23fbd | |
Dan Ballard | 5d09341277 | |
Dan Ballard | 191065f51c | |
Sarah Jamie Lewis | 47e26f18fc | |
Dan Ballard | 2bf28e2c6a | |
Sarah Jamie Lewis | af3c6940bd | |
Dan Ballard | c3fa6735f5 | |
Sarah Jamie Lewis | 8cfbc39988 | |
Sarah Jamie Lewis | fc29b10f12 | |
Dan Ballard | a49ad07b40 | |
Sarah Jamie Lewis | 34e296959a | |
Sarah Jamie Lewis | d4d7a54af1 | |
Sarah Jamie Lewis | 5139846f31 | |
Sarah Jamie Lewis | 483213c63b | |
Sarah Jamie Lewis | 546ac6c23d | |
Dan Ballard | 3a752b7397 | |
Sarah Jamie Lewis | 7540aed701 | |
Sarah Jamie Lewis | ad52f2e0c8 | |
Sarah Jamie Lewis | 337f6dc5d9 | |
Sarah Jamie Lewis | 814e6df6f6 | |
Dan Ballard | 62ea8278f3 | |
Sarah Jamie Lewis | e8a638ed29 | |
Sarah Jamie Lewis | 44fba12d21 | |
Sarah Jamie Lewis | 7516232bd4 | |
Sarah Jamie Lewis | 5be25b87c4 | |
Sarah Jamie Lewis | e13ad5d218 | |
Sarah Jamie Lewis | c397a9cdb7 | |
Sarah Jamie Lewis | 60e822cf12 | |
Sarah Jamie Lewis | 0ea2a2116e | |
Dan Ballard | 9fb9759e6a | |
Sarah Jamie Lewis | 62b87f2939 | |
Sarah Jamie Lewis | da58555104 | |
Sarah Jamie Lewis | d8cfb5c730 | |
Sarah Jamie Lewis | 0dd9ecedac | |
Sarah Jamie Lewis | 61ee9491ab | |
Sarah Jamie Lewis | 6b9cf1f164 | |
Sarah Jamie Lewis | b8326762bf | |
Dan Ballard | af9a386ae8 | |
Sarah Jamie Lewis | da5925c7b3 | |
Dan Ballard | eabee61687 | |
Dan Ballard | 102341f931 | |
Dan Ballard | e36c5bf2f9 | |
Dan Ballard | 629c9152ca | |
Dan Ballard | 9298be0a61 | |
Sarah Jamie Lewis | fc4a87e3aa | |
Dan Ballard | bef8ca083b | |
Dan Ballard | 8d0b277731 | |
Sarah Jamie Lewis | 7badbca926 | |
Dan Ballard | 708f00f678 | |
Dan Ballard | 4404977128 | |
Sarah Jamie Lewis | e29366cb49 | |
Sarah Jamie Lewis | 3a12a94a85 | |
Sarah Jamie Lewis | 8da9db87de | |
Sarah Jamie Lewis | 93adb32ca5 | |
Dan Ballard | ee9af54917 | |
Sarah Jamie Lewis | 453feae88a | |
Dan Ballard | e32e32ed27 | |
Sarah Jamie Lewis | bf1eece1e2 | |
Sarah Jamie Lewis | 9c9916e7c9 | |
Sarah Jamie Lewis | b3788b4f05 | |
Dan Ballard | 5f67f626e5 | |
Sarah Jamie Lewis | 00ca54a6a3 | |
Sarah Jamie Lewis | 5770eb4b66 | |
Sarah Jamie Lewis | ab77ad80d1 | |
Dan Ballard | 0c426a129a | |
Sarah Jamie Lewis | 0aa0d286ef | |
Sarah Jamie Lewis | 1483ddcc94 | |
Dan Ballard | 405160947b | |
Dan Ballard | 3a5668734e | |
Dan Ballard | a6406e9068 | |
Sarah Jamie Lewis | a4e1a7ede1 | |
Sarah Jamie Lewis | 77227111fd | |
Sarah Jamie Lewis | 9d2654459c | |
Sarah Jamie Lewis | 6dca8e80e6 | |
Dan Ballard | 3f4530f299 | |
Dan Ballard | 40b3207e2d | |
Sarah Jamie Lewis | fc1f910486 | |
Sarah Jamie Lewis | f71bce5b71 | |
Sarah Jamie Lewis | c01860f1de | |
Dan Ballard | c7e6cfcbc1 | |
Dan Ballard | d9acca7b1b | |
Dan Ballard | 914fe9c300 | |
Dan Ballard | ce5499419f | |
Dan Ballard | 531595e9e9 | |
Dan Ballard | 9857dff9a3 | |
Dan Ballard | 03b3d86a41 | |
Dan Ballard | a5040b7236 | |
Dan Ballard | a83b357f0f | |
Sarah Jamie Lewis | b425175fff | |
Sarah Jamie Lewis | 8570199196 | |
Dan Ballard | 1122c818f5 | |
Dan Ballard | 6714b0d8a0 | |
Sarah Jamie Lewis | 0d90219c87 | |
Sarah Jamie Lewis | 8ab82569e3 | |
Dan Ballard | a7861681e1 | |
Sarah Jamie Lewis | 106b45c758 | |
Sarah Jamie Lewis | 644ae502e5 | |
Sarah Jamie Lewis | 7bae6485f7 | |
Sarah Jamie Lewis | 04c335e7a4 | |
Sarah Jamie Lewis | 3961692817 | |
Sarah Jamie Lewis | d703a9636f | |
Dan Ballard | e4419366a4 | |
Sarah Jamie Lewis | f848316db9 | |
Sarah Jamie Lewis | a5b253f185 | |
Sarah Jamie Lewis | e7c19c7477 | |
Dan Ballard | 59df024867 | |
Sarah Jamie Lewis | 65ff084952 | |
Sarah Jamie Lewis | b3e11cfffd | |
Sarah Jamie Lewis | 0c9be47e17 | |
Dan Ballard | 3bb3a8736c | |
Sarah Jamie Lewis | 67850e8e4b | |
Dan Ballard | c8e896fa51 | |
Sarah Jamie Lewis | d1e8f71290 | |
Sarah Jamie Lewis | be8646e805 | |
Sarah Jamie Lewis | 6d42f2c76c | |
Sarah Jamie Lewis | 8429907650 | |
Sarah Jamie Lewis | c3848553d7 | |
Sarah Jamie Lewis | 3c85b8f59e | |
Sarah Jamie Lewis | d0e7e6703b | |
Sarah Jamie Lewis | 2bc47173f9 | |
Sarah Jamie Lewis | 15c68d8812 | |
Sarah Jamie Lewis | e76f2883c6 | |
Dan Ballard | 439b9b874f | |
Sarah Jamie Lewis | f5393cdb79 | |
Sarah Jamie Lewis | c0f1b674aa | |
Dan Ballard | 630713a5e4 | |
Sarah Jamie Lewis | d10a6df872 | |
Sarah Jamie Lewis | 2723a35d44 | |
Dan Ballard | 427081c937 | |
Sarah Jamie Lewis | 9d4abc3725 | |
Sarah Jamie Lewis | fa52b741bf | |
Dan Ballard | fb86fb6eae | |
Sarah Jamie Lewis | 8dd696b6ab | |
Dan Ballard | 001ad854c7 | |
Dan Ballard | af5fb678fc | |
Dan Ballard | ffa51e83a1 | |
Dan Ballard | 441845ed49 | |
Sarah Jamie Lewis | 0146436cb3 | |
Dan Ballard | 0647a2d98d | |
Dan Ballard | 0bcfe75a63 | |
Dan Ballard | ecdcef2192 | |
Dan Ballard | e6c9f7becb | |
Sarah Jamie Lewis | 9d8f73ac00 | |
Sarah Jamie Lewis | dc78117e1a | |
Dan Ballard | 59e3220bce | |
Sarah Jamie Lewis | 653ba199bc | |
Sarah Jamie Lewis | 1b45205c48 | |
Dan Ballard | 85186b2565 | |
Sarah Jamie Lewis | 3287fa79ff | |
Sarah Jamie Lewis | 111d522484 | |
Sarah Jamie Lewis | 20c854bafb | |
Dan Ballard | ffdc7b3262 | |
Dan Ballard | a3d986d9d6 | |
Sarah Jamie Lewis | 5e3387ec8a | |
Dan Ballard | a6c7682c84 | |
Dan Ballard | b29836ed3b | |
Sarah Jamie Lewis | e0bf47b6ab | |
Dan Ballard | 4bd92d854f | |
Dan Ballard | 82d1bf873f | |
Dan Ballard | 5959981fe4 | |
Dan Ballard | ab315e289a | |
Dan Ballard | 6392d67332 | |
Dan Ballard | 8f0b73af2a | |
Dan Ballard | 4e2f83ccd9 | |
Dan Ballard | dc5ba7b392 | |
Sarah Jamie Lewis | 3595f5d8d1 | |
Sarah Jamie Lewis | 1df348c0c1 | |
Sarah Jamie Lewis | 548e7f4925 | |
Dan Ballard | a20d2dffc4 | |
Dan Ballard | 2a712565e9 | |
Dan Ballard | a94fd3547b | |
Dan Ballard | c377a09748 | |
Dan Ballard | d261fbd4c0 | |
Dan Ballard | 933ca74fbc | |
Sarah Jamie Lewis | 38f317194d | |
Sarah Jamie Lewis | a4ab2ec060 | |
Dan Ballard | 47795094a0 | |
Sarah Jamie Lewis | 0d1e7bb5a0 | |
Sarah Jamie Lewis | 987b80c92b | |
Sarah Jamie Lewis | e718adad8a | |
Sarah Jamie Lewis | 0b9c159e85 | |
Sarah Jamie Lewis | a4a2af08b4 | |
Sarah Jamie Lewis | 471a729d46 | |
Dan Ballard | 1cffea5c1a | |
Sarah Jamie Lewis | e7c5b2cfa5 | |
Dan Ballard | e08114881c | |
Sarah Jamie Lewis | 6eaf95a33b | |
Dan Ballard | 0db68bcdbb | |
Dan Ballard | f64559191b | |
Dan Ballard | b8c1c7682b | |
Dan Ballard | 9812111041 | |
Dan Ballard | ecc9a3a48c | |
Dan Ballard | 523531e6be | |
Dan Ballard | ff3e60a750 | |
Dan Ballard | 5a1c66bc25 | |
Sarah Jamie Lewis | 10780ac8cb | |
Sarah Jamie Lewis | 0857d46809 | |
Dan Ballard | d7d3b2ef97 | |
Sarah Jamie Lewis | 65d5e9777d | |
Dan Ballard | 27f4c5f00e | |
Sarah Jamie Lewis | f48b6af3dd | |
Dan Ballard | d8e19de5b1 | |
Sarah Jamie Lewis | af03dd30cc | |
Sarah Jamie Lewis | 8a3867b5b3 | |
Sarah Jamie Lewis | 6237032716 | |
Dan Ballard | 915cf1a6d8 | |
Dan Ballard | c4ebed0a71 | |
Dan Ballard | 3c71bb8184 | |
Sarah Jamie Lewis | c3661d4caa | |
Sarah Jamie Lewis | 62a99797ca | |
Dan Ballard | 7cfa9432c8 | |
Dan Ballard | 1d0cb785c1 | |
Dan Ballard | 8eaa3974c9 | |
Sarah Jamie Lewis | 6cc5146744 | |
Sarah Jamie Lewis | 1fea540f9d | |
Dan Ballard | 7457246a01 | |
Sarah Jamie Lewis | 0a26a1899b | |
Sarah Jamie Lewis | 8183fbd987 | |
Sarah Jamie Lewis | f3f5f65e22 | |
Dan Ballard | c565089578 | |
Sarah Jamie Lewis | 009f99e0f5 | |
Sarah Jamie Lewis | 0894fc577b | |
Dan Ballard | b0977b31a5 | |
Sarah Jamie Lewis | 6df922d64e | |
Sarah Jamie Lewis | b70de4052d | |
Allan Christoffersen | 453558f034 | |
Allan Christoffersen | 481890b55f | |
Dan Ballard | 7122db0388 | |
Sarah Jamie Lewis | c56f40c090 | |
Sarah Jamie Lewis | c4c693144d | |
Dan Ballard | 891bf51a70 | |
Sarah Jamie Lewis | a559b0caf8 | |
Sarah Jamie Lewis | 054e5fca84 | |
Dan Ballard | 6b5f4febe7 | |
Sarah Jamie Lewis | 2c55f78913 | |
Henrik Austad | f1cfd2c30f | |
Sarah Jamie Lewis | b36e76b818 | |
Dan Ballard | 2aadea0cea | |
Dan Ballard | 423a2bce5e | |
Dan Ballard | eef40f76f9 | |
Sarah Jamie Lewis | 385f86be02 | |
Sarah Jamie Lewis | 193a9d6f89 | |
Dan Ballard | 2ade7e8e4f | |
Sarah Jamie Lewis | 12a0fc1059 | |
Sarah Jamie Lewis | 82542664ad | |
Sarah Jamie Lewis | 670d8bc343 | |
Dan Ballard | ce1db17148 | |
Dan Ballard | 018a51b76e | |
Dan Ballard | 61cdb37226 | |
Dan Ballard | 5b4778dd78 | |
Dan Ballard | 152f5fbc96 | |
Dan Ballard | 5e7272b15a | |
Dan Ballard | 9473acd438 | |
Sarah Jamie Lewis | 4fd8075497 | |
Sarah Jamie Lewis | 70eb160abc | |
Dan Ballard | 1a4dccf44a | |
Dan Ballard | 7509c20a62 | |
Dan Ballard | 68c2e1547a | |
Sarah Jamie Lewis | 705b6e02c9 | |
Sarah Jamie Lewis | 137de57e83 | |
Dan Ballard | 6859780873 | |
Sarah Jamie Lewis | dab09c6acb | |
Sarah Jamie Lewis | 7bf2e15009 | |
Sarah Jamie Lewis | a0f8be2d53 | |
Sarah Jamie Lewis | 02407c5abe | |
Sarah Jamie Lewis | 635e383f65 | |
Dan Ballard | 1ec9be3d9a | |
Sarah Jamie Lewis | fd886e7315 | |
Dan Ballard | 387816ea0f | |
Dan Ballard | 3cb6c9d9f4 | |
Sarah Jamie Lewis | f1688c5f8f | |
Sarah Jamie Lewis | d5296d2211 | |
Sarah Jamie Lewis | 953971980f | |
Sarah Jamie Lewis | 7e59d1a526 | |
Dan Ballard | 783d666486 | |
Dan Ballard | 040ba80480 | |
Dan Ballard | 8ba54469eb | |
Dan Ballard | 706d1da518 | |
Dan Ballard | b5511ae723 | |
Dan Ballard | 4c47198977 | |
erinn | 9a17852533 | |
Sarah Jamie Lewis | 2a07ba8ed7 | |
Sarah Jamie Lewis | 2e5ee796fa | |
Dan Ballard | d1d3f23f82 | |
Dan Ballard | fa6e399aab | |
Dan Ballard | ddefcb8ff2 | |
Dan Ballard | b382c3d349 | |
Dan Ballard | c550437aa5 | |
erinn | e6246cf44a | |
Sarah Jamie Lewis | d71574a831 | |
Sarah Jamie Lewis | 62bca86c19 | |
erinn | 729ff6811e | |
Sarah Jamie Lewis | bf4cfde7df | |
Sarah Jamie Lewis | 403454d6b8 | |
Sarah Jamie Lewis | d902ba5cce | |
Sarah Jamie Lewis | 5b5fe586e8 | |
Sarah Jamie Lewis | b280765631 | |
Sarah Jamie Lewis | 2a2d808b60 | |
Sarah Jamie Lewis | d158d7d619 | |
Sarah Jamie Lewis | c6192ef736 | |
Sarah Jamie Lewis | 3d85883f8e | |
erinn | e22db92dc1 | |
Sarah Jamie Lewis | dd69afc98b | |
Sarah Jamie Lewis | ab9d6929be | |
Sarah Jamie Lewis | cd4c778b71 | |
Dan Ballard | 19a202a04c | |
Dan Ballard | be65417f27 | |
Dan Ballard | 8a9ee402bf | |
Dan Ballard | 1a9f0763d7 | |
Dan Ballard | a82ade8663 | |
Dan Ballard | 715b2c6876 | |
Dan Ballard | ca03ddbc53 | |
Dan Ballard | 0853832a38 | |
Dan Ballard | f818d4f2f8 | |
Sarah Jamie Lewis | 6814515186 | |
Sarah Jamie Lewis | d84850af49 | |
Dan Ballard | a4ce168aec | |
Sarah Jamie Lewis | 2bff77983b | |
Sarah Jamie Lewis | 35ae5773f7 | |
Sarah Jamie Lewis | 6276b022dc | |
Sarah Jamie Lewis | 5c76628578 | |
Dan Ballard | dc587f95f0 | |
Dan Ballard | 04cf1e16c2 | |
Dan Ballard | d96e251650 | |
Dan Ballard | c3bc961a47 | |
Sarah Jamie Lewis | c672574bb2 | |
Sarah Jamie Lewis | 05f3cacdbd | |
Sarah Jamie Lewis | 2e3d02bbe9 | |
Dan Ballard | 23b6eddf6a | |
Dan Ballard | c838176e3b | |
Dan Ballard | a9d272e414 | |
Dan Ballard | 598251a624 | |
Sarah Jamie Lewis | def222a8ab | |
Sarah Jamie Lewis | 748326e13f | |
erinn | 508592f80c | |
Sarah Jamie Lewis | d27cc0e64e | |
Sarah Jamie Lewis | e359afbdab | |
Sarah Jamie Lewis | 92374ad112 | |
erinn | 0a3837c8b5 | |
Sarah Jamie Lewis | 19777afb79 | |
Sarah Jamie Lewis | 9931521910 | |
erinn | 7dcc1c863a | |
Sarah Jamie Lewis | 13c1a52442 | |
Sarah Jamie Lewis | 6364ebffc6 | |
Sarah Jamie Lewis | d095971cb3 | |
Sarah Jamie Lewis | 797279d6d7 | |
Sarah Jamie Lewis | d0fecbd545 | |
Dan Ballard | ccdd7d0e27 | |
Dan Ballard | 889d398343 | |
Dan Ballard | 589bc4c36c | |
Dan Ballard | 793b6e2e1a | |
Dan Ballard | d5cb37ed9c | |
Dan Ballard | e7b9f5bb96 | |
Dan Ballard | 5e8f712a90 | |
Sarah Jamie Lewis | 2495814869 | |
Dan Ballard | 08b9dfed5f | |
Sarah Jamie Lewis | 19f73eb075 | |
Dan Ballard | 0fe6f21a75 | |
Sarah Jamie Lewis | 52e22c085f | |
Sarah Jamie Lewis | 47348f3ad7 | |
Dan Ballard | 706c1fb354 | |
erinn | e99fc45a28 | |
Sarah Jamie Lewis | ca44fd798c | |
Sarah Jamie Lewis | 1700306c78 | |
Sarah Jamie Lewis | da3234e3e4 | |
Sarah Jamie Lewis | 303b70d751 | |
Sarah Jamie Lewis | cd1bf07fba | |
Sarah Jamie Lewis | b3f06d6765 | |
Sarah Jamie Lewis | c6e64a3a5f | |
erinn | 9d10b9ea8d | |
Sarah Jamie Lewis | 7257e2bca0 | |
Sarah Jamie Lewis | 5494cb5de0 | |
Sarah Jamie Lewis | d6ecf87255 | |
Sarah Jamie Lewis | ae6f0dd456 | |
Sarah Jamie Lewis | ed671d32bc | |
Sarah Jamie Lewis | daa89bf6e7 | |
erinn | 24787adc9c | |
Sarah Jamie Lewis | a3e2da8469 | |
Sarah Jamie Lewis | 9d3d5b06e5 | |
Sarah Jamie Lewis | bee3ae6e7b | |
erinn | 1bd2195be4 | |
Sarah Jamie Lewis | 26f32a0790 | |
Sarah Jamie Lewis | 958be3e8f7 | |
Sarah Jamie Lewis | 4cdbb04243 | |
Sarah Jamie Lewis | 92422de98e | |
erinn | 5e4c190e41 | |
Sarah Jamie Lewis | 659e89d626 | |
Sarah Jamie Lewis | 306a9c4de5 |
39
.drone.yml
|
@ -8,7 +8,7 @@ clone:
|
|||
|
||||
steps:
|
||||
- name: clone
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
environment:
|
||||
buildbot_key_b64:
|
||||
from_secret: buildbot_key_b64
|
||||
|
@ -24,7 +24,7 @@ steps:
|
|||
- git checkout $DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -47,7 +47,7 @@ steps:
|
|||
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
|
||||
|
||||
- name: build-linux
|
||||
image: openpriv/flutter-desktop:linux-fstable-2.8.0
|
||||
image: openpriv/flutter-desktop:linux-fstable-3.3.9
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -60,8 +60,20 @@ steps:
|
|||
- tar -czf cwtch-`cat ../VERSION`.tar.gz cwtch
|
||||
- rm -r cwtch
|
||||
|
||||
- name: linux-ui-tests
|
||||
image: openpriv/flutter-desktop:linux-fstable-3.3.9
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
commands:
|
||||
- # todo: add xvfb to openpriv/flutter-desktop:linux-fstable-3.7
|
||||
- sudo apt update
|
||||
- sudo apt-get install -y xvfb
|
||||
- ./fetch-tor.sh
|
||||
- ./run-tests-headless.sh 02_save_load
|
||||
|
||||
- name: test-build-android
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
when:
|
||||
event: pull_request
|
||||
volumes:
|
||||
|
@ -71,7 +83,7 @@ steps:
|
|||
- flutter build apk --debug
|
||||
|
||||
- name: build-android
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
when:
|
||||
event: push
|
||||
environment:
|
||||
|
@ -95,7 +107,7 @@ steps:
|
|||
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
|
||||
|
||||
- name: widget-tests
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -177,7 +189,7 @@ clone:
|
|||
|
||||
steps:
|
||||
- name: clone
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-3.3.8
|
||||
environment:
|
||||
buildbot_key_b64:
|
||||
from_secret: buildbot_key_b64
|
||||
|
@ -195,16 +207,15 @@ steps:
|
|||
- git checkout $Env:DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-3.3.8
|
||||
commands:
|
||||
- powershell -command "Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip"
|
||||
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }"
|
||||
- git describe --tags --abbrev=1 > VERSION
|
||||
- powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE
|
||||
- .\fetch-tor-win.ps1
|
||||
- .\fetch-libcwtch-go.ps1
|
||||
|
||||
- name: build-windows
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-3.3.8
|
||||
commands:
|
||||
- flutter pub get
|
||||
- $Env:version += type .\VERSION
|
||||
|
@ -230,7 +241,7 @@ steps:
|
|||
status: [ success ]
|
||||
environment:
|
||||
pfx:
|
||||
from_secret: pfx
|
||||
from_secret: pfx2022_b64
|
||||
pfx_pass:
|
||||
from_secret: pfx_pass
|
||||
commands:
|
||||
|
@ -246,6 +257,8 @@ steps:
|
|||
- echo $Env:pfx > codesign.pfx.b64
|
||||
- certutil -decode codesign.pfx.b64 codesign.pfx
|
||||
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\cwtch.exe
|
||||
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\libCwtch.dll
|
||||
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\flutter_windows.dll
|
||||
- copy windows\runner\resources\knot_128.ico $Env:releasedir\cwtch.ico
|
||||
- makensis windows\nsis\cwtch-installer.nsi
|
||||
- move windows\nsis\cwtch-installer.exe cwtch-installer.exe
|
||||
|
@ -261,7 +274,7 @@ steps:
|
|||
- move *.sha512 deploy\$Env:builddir
|
||||
|
||||
- name: deploy-windows
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-3.3.8
|
||||
when:
|
||||
event: push
|
||||
status: [ success ]
|
||||
|
|
|
@ -40,10 +40,31 @@ app.*.symbols
|
|||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Tor
|
||||
*data-dir*
|
||||
|
||||
# Test Artificats
|
||||
*.log
|
||||
flutter_gherkin
|
||||
run-tests.env
|
||||
report.json
|
||||
package.
|
||||
|
||||
# Compiled Libs
|
||||
linux/tor
|
||||
linux/libCwtch.so
|
||||
android/cwtch/cwtch.aar
|
||||
android/app/src/main/jniLibs/*/libtor.so
|
||||
*.dylib
|
||||
integration_test/gherkin_suite_test.g.dart
|
||||
integration_test/gherkin_suite_test.dart
|
||||
integration_test/gherkin/
|
||||
integration_test/CustomSteps.md
|
||||
analysis_options.yaml
|
||||
integration_test/env/default/tor
|
||||
linux/Tor
|
||||
linux/tor.tar.gz
|
||||
|
||||
coverage
|
||||
test/failures
|
||||
.gradle
|
||||
|
|
|
@ -1 +1 @@
|
|||
2022-01-07-16-29-v1.5.4
|
||||
2022-12-12-17-58-v1.10.1-3-g3d0a3a5
|
|
@ -1 +1 @@
|
|||
2022-01-07-21-30-v1.5.4
|
||||
2022-12-12-22-59-v1.10.1-3-g3d0a3a5
|
|
@ -65,7 +65,7 @@ To build a release version and load normal profiles, use `build-release.sh X` in
|
|||
### Building on MacOS
|
||||
|
||||
- Cocaopods is required, you may need to `gem install cocaopods -v 1.9.3`
|
||||
- copy `libCwtch.dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it
|
||||
- copy `libCwtch.x64.dylib` and `libCwtch.arm/dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it
|
||||
- run `fetch-tor-macos.sh` to fetch Tor or Download and install Tor Browser and `cp -r /Applications/Tor\ Browser.app/Contents/MacOS/Tor ./macos/`
|
||||
- `flutter build macos`
|
||||
- optional: launch cwtch-ui release build with `./build/macos/Build/Products/Release/Cwtch.app/Contents/MacOS/Cwtch`
|
||||
|
|
|
@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
@ -48,9 +48,11 @@ android {
|
|||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "im.cwtch.flwtch"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 31
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
@ -89,16 +91,27 @@ dependencies {
|
|||
implementation project(':cwtch')
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
|
||||
implementation "com.airbnb.android:lottie:3.5.0"
|
||||
implementation "com.airbnb.android:lottie:4.2.1"
|
||||
implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0"
|
||||
implementation "com.android.support.constraint:constraint-layout:2.0.4"
|
||||
|
||||
// Test Dependencies
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/test/#1.2.0
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
// WorkManager
|
||||
|
||||
// (Java only)
|
||||
//implementation("androidx.work:work-runtime:$work_version")
|
||||
|
||||
// Kotlin + coroutines
|
||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||
// 2022.06: upgraded from 2.5 to 2.7 for android 12
|
||||
// err: "requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent"
|
||||
// as per https://github.com/flutter/flutter/issues/93609
|
||||
implementation 'androidx.work:work-runtime-ktx:2.7.0'
|
||||
|
||||
// optional - RxJava2 support
|
||||
//implementation("androidx.work:work-rxjava2:$work_version")
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package im.cwtch.flwtch;
|
||||
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import dev.flutter.plugins.integration_test.FlutterTestRunner;
|
||||
import org.junit.Rule;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(FlutterTestRunner.class)
|
||||
public class MainActivityTest {
|
||||
@Rule
|
||||
public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class, true, false);
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:name="${applicationName}"
|
||||
android:label="Cwtch"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/knott">
|
||||
|
@ -16,7 +16,8 @@
|
|||
android:theme="@style/NormalTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
|
@ -46,7 +47,15 @@
|
|||
<!--Needed to run in background (lol)-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
||||
<!-- Ability to ask user to exempt app from power management (which can kill it more frequently especially on some devices.
|
||||
Allows app to use ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS -->
|
||||
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- TODO when we support sdk 31
|
||||
<uses-permission-sdk-23 android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
||||
-->
|
||||
|
||||
<!--Needed to check if activity is foregrounded or if messages from the service should be queued-->
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
|
||||
<queries>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// Generated file.
|
||||
//
|
||||
// If you wish to remove Flutter's multidex support, delete this entire file.
|
||||
//
|
||||
// Modifications to this file should be done in a copy under a different name
|
||||
// as this file may be regenerated.
|
||||
|
||||
package io.flutter.app;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.multidex.MultiDex;
|
||||
|
||||
/**
|
||||
* Extension of {@link android.app.Application}, adding multidex support.
|
||||
*/
|
||||
public class FlutterMultiDexApplication extends Application {
|
||||
@Override
|
||||
@CallSuper
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
MultiDex.install(this);
|
||||
}
|
||||
}
|
|
@ -1,24 +1,27 @@
|
|||
package im.cwtch.flwtch
|
||||
|
||||
import android.app.*
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.*
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import cwtch.Cwtch
|
||||
import io.flutter.FlutterInjector
|
||||
import org.json.JSONObject
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import android.net.Uri
|
||||
|
||||
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||
CoroutineWorker(context, parameters) {
|
||||
|
@ -29,13 +32,26 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
private var notificationID: MutableMap<String, Int> = mutableMapOf()
|
||||
private var notificationIDnext: Int = 1
|
||||
|
||||
private var notificationSimple: String? = null
|
||||
private var notificationConversationInfo: String? = null
|
||||
|
||||
private val TAG: String = "FlwtchWorker.kt"
|
||||
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
// Hack to uncomment and deploy if your device has zombie workers you need to kill
|
||||
// We need a proper solution but this will clear those out for now
|
||||
/*if (notificationSimple == null) {
|
||||
Log.e("FlwtchWorker", "doWork found notificationSimple is null, app has not started, this is a stale thread, terminating")
|
||||
return Result.failure()
|
||||
}*/
|
||||
|
||||
val method = inputData.getString(KEY_METHOD)
|
||||
?: return Result.failure()
|
||||
val args = inputData.getString(KEY_ARGS)
|
||||
?: return Result.failure()
|
||||
// Mark the Worker as important
|
||||
val progress = "Cwtch is keeping Tor running in the background"//todo:translate
|
||||
val progress = "Cwtch is keeping Tor running in the background" // TODO: translate
|
||||
setForeground(createForegroundInfo(progress))
|
||||
return handleCwtch(method, args)
|
||||
}
|
||||
|
@ -49,37 +65,79 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
}
|
||||
|
||||
private fun handleCwtch(method: String, args: String): Result {
|
||||
if (method != "Start") {
|
||||
if (Cwtch.started() != 1.toLong()) {
|
||||
Log.e(TAG, "libCwtch-go reports it is not initialized yet")
|
||||
return Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
val a = JSONObject(args)
|
||||
when (method) {
|
||||
"Start" -> {
|
||||
Log.i("FlwtchWorker.kt", "handleAppInfo Start")
|
||||
Log.i(TAG, "handleAppInfo Start")
|
||||
val appDir = (a.get("appDir") as? String) ?: ""
|
||||
val torPath = (a.get("torPath") as? String) ?: "tor"
|
||||
Log.i("FlwtchWorker.kt", "appDir: '$appDir' torPath: '$torPath'")
|
||||
Log.i(TAG, "appDir: '$appDir' torPath: '$torPath'")
|
||||
|
||||
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
|
||||
|
||||
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
|
||||
Log.i(TAG, "startCwtch success, starting coroutine AppbusEvent loop...")
|
||||
val downloadIDs = mutableMapOf<String, Int>()
|
||||
while(true) {
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
while (true) {
|
||||
try {
|
||||
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
|
||||
// TODO replace this notification block with the NixNotification manager in dart as it has access to contact names and also needs less working around
|
||||
|
||||
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
|
||||
val data = JSONObject(evt.Data)
|
||||
val handle = if (evt.EventType == "NewMessageFromPeer") data.getString("RemotePeer") else data.getString("GroupID");
|
||||
val handle = data.getString("RemotePeer");
|
||||
val conversationId = data.getInt("ConversationID").toString();
|
||||
val notificationChannel = if (evt.EventType == "NewMessageFromPeer") handle else conversationId
|
||||
if (data["RemotePeer"] != data["ProfileOnion"]) {
|
||||
val notification = data["notification"]
|
||||
|
||||
if (notification == "SimpleEvent") {
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createMessageNotificationChannel(handle, handle)
|
||||
createMessageNotificationChannel("Cwtch", "Cwtch")
|
||||
} else {
|
||||
// If earlier version channel ID is not used
|
||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||
""
|
||||
}
|
||||
|
||||
val loader = FlutterInjector.instance().flutterLoader()
|
||||
val key = loader.getLookupKeyForAsset("assets/" + data.getString("Picture"))//"assets/profiles/001-centaur.png")
|
||||
val fh = applicationContext.assets.open(key)
|
||||
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
|
||||
intent.action = Intent.ACTION_RUN
|
||||
intent.putExtra("EventType", "NotificationClicked")
|
||||
}
|
||||
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle("Cwtch")
|
||||
.setContentText(notificationSimple ?: "New Message")
|
||||
.setSmallIcon(R.mipmap.knott_transparent)
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(getNotificationID("Cwtch", "Cwtch"), newNotification)
|
||||
} else if (notification == "ContactInfo") {
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createMessageNotificationChannel(notificationChannel, notificationChannel)
|
||||
} else {
|
||||
// If earlier version channel ID is not used
|
||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||
""
|
||||
}
|
||||
val loader = FlutterInjector.instance().flutterLoader()
|
||||
Log.i(TAG, "notification for " + evt.EventType + " " + handle + " " + conversationId + " " + channelId)
|
||||
Log.i(TAG, data.toString());
|
||||
val key = loader.getLookupKeyForAsset(data.getString("picture"))//"assets/profiles/001-centaur.png")
|
||||
val fh = applicationContext.assets.open(key)
|
||||
|
||||
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
|
||||
intent.action = Intent.ACTION_RUN
|
||||
|
@ -90,13 +148,17 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
|
||||
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle(data.getString("Nick"))
|
||||
.setContentText("New message")//todo: translate
|
||||
.setContentText((notificationConversationInfo
|
||||
?: "New Message From %1").replace("%1", data.getString("Nick")))
|
||||
.setLargeIcon(BitmapFactory.decodeStream(fh))
|
||||
.setSmallIcon(R.mipmap.knott_transparent)
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), handle), newNotification)
|
||||
|
||||
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), channelId), newNotification)
|
||||
}
|
||||
|
||||
}
|
||||
} else if (evt.EventType == "FileDownloadProgressUpdate") {
|
||||
try {
|
||||
|
@ -136,18 +198,18 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
|
||||
}
|
||||
} else if (evt.EventType == "FileDownloaded") {
|
||||
Log.d("FlwtchWorker", "file downloaded!");
|
||||
Log.d(TAG, "file downloaded!");
|
||||
val data = JSONObject(evt.Data);
|
||||
val tempFile = data.getString("TempFile");
|
||||
val fileKey = data.getString("FileKey");
|
||||
if (tempFile != "" && tempFile != data.getString("FilePath")) {
|
||||
val filePath = data.getString("FilePath");
|
||||
Log.i("FlwtchWorker", "moving "+tempFile+" to "+filePath);
|
||||
Log.i(TAG, "moving " + tempFile + " to " + filePath);
|
||||
val sourcePath = Paths.get(tempFile);
|
||||
val targetUri = Uri.parse(filePath);
|
||||
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
|
||||
val bytesWritten = Files.copy(sourcePath, os);
|
||||
Log.d("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes");
|
||||
Log.d("TAG", "copied " + bytesWritten.toString() + " bytes");
|
||||
if (bytesWritten != 0L) {
|
||||
os?.flush();
|
||||
os?.close();
|
||||
|
@ -155,7 +217,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
}
|
||||
}
|
||||
if (downloadIDs.containsKey(fileKey)) {
|
||||
notificationManager.cancel(downloadIDs.get(fileKey)?:0);
|
||||
notificationManager.cancel(downloadIDs.get(fileKey) ?: 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,205 +228,23 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
intent.putExtra("EventID", evt.EventID)
|
||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
"ReconnectCwtchForeground" -> {
|
||||
Cwtch.reconnectCwtchForeground()
|
||||
}
|
||||
"CreateProfile" -> {
|
||||
val nick = (a.get("nick") as? String) ?: ""
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.createProfile(nick, pass)
|
||||
}
|
||||
"LoadProfiles" -> {
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.loadProfiles(pass)
|
||||
}
|
||||
"ChangePassword" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val pass = (a.get("OldPass") as? String) ?: ""
|
||||
val passNew = (a.get("NewPass") as? String) ?: ""
|
||||
val passNew2 = (a.get("NewPassAgain") as? String) ?: ""
|
||||
Cwtch.changePassword(profile, pass, passNew, passNew2)
|
||||
}
|
||||
"GetMessage" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val indexI = a.getInt("index").toLong()
|
||||
Log.d("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString())
|
||||
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build())
|
||||
}
|
||||
"GetMessageByID" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val id = a.getInt("id").toLong()
|
||||
return Result.success(Data.Builder().putString("result", Cwtch.getMessageByID(profile, conversation, id)).build())
|
||||
}
|
||||
"GetMessageByContentHash" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val contentHash = (a.get("contentHash") as? String) ?: ""
|
||||
return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, conversation, contentHash)).build())
|
||||
}
|
||||
"UpdateMessageAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val channel = a.getInt("chanenl").toLong()
|
||||
val midx = a.getInt("midx").toLong()
|
||||
val key = (a.get("key") as? String) ?: ""
|
||||
val value = (a.get("value") as? String) ?: ""
|
||||
Cwtch.setMessageAttribute(profile, conversation, channel, midx, key, value)
|
||||
}
|
||||
"AcceptConversation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.acceptConversation(profile, conversation)
|
||||
}
|
||||
"BlockContact" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.blockContact(profile, conversation)
|
||||
}
|
||||
"UnblockContact" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.unblockContact(profile, conversation)
|
||||
}
|
||||
"SendMessage" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val message = (a.get("message") as? String) ?: ""
|
||||
Cwtch.sendMessage(profile, conversation, message)
|
||||
}
|
||||
"SendInvitation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val target = a.getInt("target").toLong()
|
||||
Cwtch.sendInvitation(profile, conversation, target)
|
||||
}
|
||||
"ShareFile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val filepath = (a.get("filepath") as? String) ?: ""
|
||||
Cwtch.shareFile(profile, conversation, filepath)
|
||||
}
|
||||
"DownloadFile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val filepath = (a.get("filepath") as? String) ?: ""
|
||||
val manifestpath = (a.get("manifestpath") as? String) ?: ""
|
||||
val filekey = (a.get("filekey") as? String) ?: ""
|
||||
// FIXME: Prevent spurious calls by Intent
|
||||
if (profile != "") {
|
||||
Cwtch.downloadFile(profile, conversation, filepath, manifestpath, filekey)
|
||||
}
|
||||
}
|
||||
"CheckDownloadStatus" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val fileKey = (a.get("fileKey") as? String) ?: ""
|
||||
Cwtch.checkDownloadStatus(profile, fileKey)
|
||||
}
|
||||
"VerifyOrResumeDownload" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val fileKey = (a.get("fileKey") as? String) ?: ""
|
||||
Cwtch.verifyOrResumeDownload(profile, conversation, fileKey)
|
||||
}
|
||||
"SendProfileEvent" -> {
|
||||
val onion = (a.get("onion") as? String) ?: ""
|
||||
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
||||
Cwtch.sendProfileEvent(onion, jsonEvent)
|
||||
}
|
||||
"SendAppEvent" -> {
|
||||
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
||||
Cwtch.sendAppEvent(jsonEvent)
|
||||
}
|
||||
"ResetTor" -> {
|
||||
Cwtch.resetTor()
|
||||
}
|
||||
"ImportBundle" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val bundle = (a.get("bundle") as? String) ?: ""
|
||||
Cwtch.importBundle(profile, bundle)
|
||||
}
|
||||
"CreateGroup" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val server = (a.get("server") as? String) ?: ""
|
||||
val groupName = (a.get("groupName") as? String) ?: ""
|
||||
Cwtch.createGroup(profile, server, groupName)
|
||||
}
|
||||
"DeleteProfile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.deleteProfile(profile, pass)
|
||||
}
|
||||
"ArchiveConversation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.archiveConversation(profile, conversation)
|
||||
}
|
||||
"DeleteConversation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.deleteContact(profile, conversation)
|
||||
}
|
||||
"SetProfileAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setProfileAttribute(profile, key, v)
|
||||
}
|
||||
"SetConversationAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setConversationAttribute(profile, conversation, key, v)
|
||||
}
|
||||
"Shutdown" -> {
|
||||
Cwtch.shutdownCwtch();
|
||||
if (evt.EventType == "Shutdown") {
|
||||
Log.i(TAG, "processing shutdown event, exiting FlwtchWorker/Start()...");
|
||||
return Result.success()
|
||||
}
|
||||
"LoadServers" -> {
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
Cwtch.loadServers(password)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in handleCwtch: " + e.toString() + " :: " + e.getStackTrace());
|
||||
}
|
||||
"CreateServer" -> {
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
val desc = (a.get("Description") as? String) ?: ""
|
||||
val autostart = (a.get("Autostart") as? Boolean) ?: false
|
||||
Cwtch.createServer(password, desc, autostart)
|
||||
}
|
||||
"DeleteServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
Cwtch.deleteServer(serverOnion, password)
|
||||
}
|
||||
"LaunchServers" -> {
|
||||
Cwtch.launchServers()
|
||||
}
|
||||
"LaunchServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
Cwtch.launchServer(serverOnion)
|
||||
}
|
||||
"StopServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
Cwtch.stopServer(serverOnion)
|
||||
}
|
||||
"StopServers" -> {
|
||||
Cwtch.stopServers()
|
||||
}
|
||||
"DestroyServers" -> {
|
||||
Cwtch.destroyServers()
|
||||
}
|
||||
"SetServerAttribute" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setServerAttribute(serverOnion, key, v)
|
||||
// Event passing translations from Flutter to Kotlin worker scope so the worker can use them
|
||||
"L10nInit" -> {
|
||||
notificationSimple = (a.get("notificationSimple") as? String) ?: "New Message"
|
||||
notificationConversationInfo = (a.get("notificationConversationInfo") as? String)
|
||||
?: "New Message From "
|
||||
}
|
||||
else -> {
|
||||
Log.i("FlwtchWorker", "unknown command: " + method);
|
||||
Log.i(TAG, "unknown command: " + method);
|
||||
return Result.failure()
|
||||
}
|
||||
}
|
||||
|
@ -375,8 +255,8 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
// ongoing notification.
|
||||
private fun createForegroundInfo(progress: String): ForegroundInfo {
|
||||
val id = "flwtch"
|
||||
val title = "Flwtch"
|
||||
val cancel = "Shut down"//todo: translate
|
||||
val title = "Flwtch" // TODO: change
|
||||
val cancel = "Shut down" // TODO: translate
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createForegroundNotificationChannel(id, id)
|
||||
|
@ -390,6 +270,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
intent.action = Intent.ACTION_RUN
|
||||
intent.putExtra("EventType", "ShutdownClicked")
|
||||
}
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
val notification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle(title)
|
||||
|
@ -399,7 +283,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
.setOngoing(true)
|
||||
// Add the cancel action to the notification which can
|
||||
// be used to cancel the worker
|
||||
.addAction(android.R.drawable.ic_delete, cancel, PendingIntent.getActivity(applicationContext, 2, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.addAction(android.R.drawable.ic_delete, cancel, PendingIntent.getActivity(applicationContext, 2, cancelIntent, flags))
|
||||
.build()
|
||||
|
||||
return ForegroundInfo(101, notification)
|
||||
|
|
|
@ -1,46 +1,48 @@
|
|||
package im.cwtch.flwtch
|
||||
|
||||
import SplashView
|
||||
import android.annotation.TargetApi
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.annotation.NonNull
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
import android.util.Log
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.*
|
||||
import io.flutter.embedding.android.SplashScreen
|
||||
import cwtch.Cwtch
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.android.SplashScreen
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import io.flutter.plugin.common.ErrorLogResult
|
||||
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.content.ContentUris
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.database.Cursor
|
||||
import android.provider.MediaStore
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
override fun provideSplashScreen(): SplashScreen? = SplashView()
|
||||
|
||||
|
||||
// Channel to get app info
|
||||
private val CHANNEL_APP_INFO = "test.flutter.dev/applicationInfo"
|
||||
private val CALL_APP_INFO = "getNativeLibDir"
|
||||
private val ANDROID_SETTINGS_CHANNEL_NAME = "androidSettings"
|
||||
private val ANDROID_SETTINGS_CHANGE_NAME= "androidSettingsChanged"
|
||||
private var andoidSettingsChangeChannel: MethodChannel? = null
|
||||
private val CALL_ASK_BATTERY_EXEMPTION = "requestBatteryExemption"
|
||||
private val CALL_IS_BATTERY_EXEMPT = "isBatteryExempt"
|
||||
|
||||
// Channel to get cwtch api calls on
|
||||
private val CHANNEL_CWTCH = "cwtch"
|
||||
|
@ -52,6 +54,7 @@ class MainActivity: FlutterActivity() {
|
|||
private val CHANNEL_NOTIF_CLICK = "im.cwtch.flwtch/notificationClickHandler"
|
||||
private val CHANNEL_SHUTDOWN_CLICK = "im.cwtch.flwtch/shutdownClickHandler"
|
||||
|
||||
private val TAG: String = "MainActivity.kt"
|
||||
// WorkManager tag applied to all Start() infinite coroutines
|
||||
val WORKER_TAG = "cwtchEventBusWorker"
|
||||
|
||||
|
@ -62,11 +65,27 @@ class MainActivity: FlutterActivity() {
|
|||
// "Download to..." prompt extra arguments
|
||||
private val FILEPICKER_REQUEST_CODE = 234
|
||||
private val PREVIEW_EXPORT_REQUEST_CODE = 235
|
||||
private val PROFILE_EXPORT_REQUEST_CODE = 236
|
||||
private val REQUEST_DOZE_WHITELISTING_CODE:Int = 9
|
||||
private var dlToProfile = ""
|
||||
private var dlToHandle = ""
|
||||
private var dlToHandle = 0
|
||||
private var dlToFileKey = ""
|
||||
private var exportFromPath = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: android.os.Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
// Todo: when we support SDK 31
|
||||
// hideOverlay()
|
||||
}
|
||||
|
||||
/*
|
||||
@TargetApi(31)
|
||||
fun hideOverlay() {
|
||||
window.setHideOverlayWindows(true);
|
||||
}
|
||||
*/
|
||||
|
||||
// handles clicks received from outside the app (ie, notifications)
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
|
@ -93,28 +112,35 @@ class MainActivity: FlutterActivity() {
|
|||
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
|
||||
super.onActivityResult(requestCode, result, intent);
|
||||
|
||||
// has null intent and data
|
||||
if (requestCode == REQUEST_DOZE_WHITELISTING_CODE) {
|
||||
// 0 == "battery optimized" (still)
|
||||
// -1 == "no battery optimization" (exempt!)
|
||||
andoidSettingsChangeChannel!!.invokeMethod("powerExemptionChange", result == -1)
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent == null || intent!!.getData() == null) {
|
||||
Log.i("MainActivity:onActivityResult", "user canceled activity");
|
||||
Log.i(TAG, "user canceled activity");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestCode == FILEPICKER_REQUEST_CODE) {
|
||||
val filePath = intent!!.getData().toString();
|
||||
val manifestPath = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.dlToFileKey).toString();
|
||||
Log.d("MainActivity:FILEPICKER_REQUEST_CODE", "DownloadableFileCreated");
|
||||
handleCwtch(MethodCall("DownloadFile", mapOf(
|
||||
"ProfileOnion" to this.dlToProfile,
|
||||
"handle" to this.dlToHandle,
|
||||
"conversation" to this.dlToHandle.toInt(),
|
||||
"filepath" to filePath,
|
||||
"manifestpath" to manifestPath,
|
||||
"filekey" to this.dlToFileKey
|
||||
)), ErrorLogResult(""));//placeholder; this Result is never actually invoked
|
||||
} else if (requestCode == PREVIEW_EXPORT_REQUEST_CODE) {
|
||||
val targetPath = intent!!.getData().toString()
|
||||
var srcFile = File(this.exportFromPath)
|
||||
Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file")
|
||||
try {
|
||||
val sourcePath = Paths.get(this.exportFromPath);
|
||||
val targetUri = Uri.parse(targetPath);
|
||||
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
|
||||
val targetUri = intent!!.getData();
|
||||
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri!!);
|
||||
val bytesWritten = Files.copy(sourcePath, os);
|
||||
Log.d("MainActivity:PREVIEW_EXPORT", "copied " + bytesWritten.toString() + " bytes");
|
||||
if (bytesWritten != 0L) {
|
||||
|
@ -122,6 +148,26 @@ class MainActivity: FlutterActivity() {
|
|||
os?.close();
|
||||
//Files.delete(sourcePath);
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d("MainActivity:PREVIEW_EXPORT FAILED", e.toString());
|
||||
}
|
||||
} else if (requestCode == PROFILE_EXPORT_REQUEST_CODE ) {
|
||||
val srcFile = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.exportFromPath).toString();
|
||||
Log.i("MainActivity:EXPORT_PROFILE", "exporting profile: " + srcFile);
|
||||
try {
|
||||
val sourcePath = Paths.get(srcFile);
|
||||
val targetUri = intent!!.getData();
|
||||
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri!!);
|
||||
val bytesWritten = Files.copy(sourcePath, os);
|
||||
Log.d("MainActivity:EXPORT_PROFILE", "copied " + bytesWritten.toString() + " bytes");
|
||||
if (bytesWritten != 0L) {
|
||||
os?.flush();
|
||||
os?.close();
|
||||
//Files.delete(sourcePath);
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d("MainActivity:EXPORT_PROFILE FAILED", e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,10 +178,13 @@ class MainActivity: FlutterActivity() {
|
|||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) }
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) }
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ANDROID_SETTINGS_CHANNEL_NAME).setMethodCallHandler { call, result -> handleAndroidSettings(call, result) }
|
||||
notificationClickChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NOTIF_CLICK)
|
||||
shutdownClickChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_SHUTDOWN_CLICK)
|
||||
andoidSettingsChangeChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ANDROID_SETTINGS_CHANGE_NAME)
|
||||
}
|
||||
|
||||
// MethodChannel CHANNEL_APP_INFO handler (Flutter Channel for requests for Android environment info)
|
||||
private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
when (call.method) {
|
||||
CALL_APP_INFO -> result.success(getNativeLibDir())
|
||||
|
@ -144,6 +193,30 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// MethodChannel ANDROID_SETTINGS_CHANNEL_NAME handler (Flutter Channel for requests for Android settings)
|
||||
// Called from lib/view/globalsettingsview.dart
|
||||
private fun handleAndroidSettings(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
when (call.method) {
|
||||
CALL_IS_BATTERY_EXEMPT -> result.success(checkIgnoreBatteryOpt() ?: false);
|
||||
CALL_ASK_BATTERY_EXEMPTION -> { requestBatteryExemption(); result.success(null); }
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
private fun checkIgnoreBatteryOpt(): Boolean {
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return powerManager.isIgnoringBatteryOptimizations(this.packageName) ?: false;
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
private fun requestBatteryExemption() {
|
||||
val i = Intent()
|
||||
i.action = ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
i.data = Uri.parse("package:" + this.packageName)
|
||||
startActivityForResult(i, REQUEST_DOZE_WHITELISTING_CODE);
|
||||
}
|
||||
|
||||
private fun getNativeLibDir(): String {
|
||||
val ainfo = this.applicationContext.packageManager.getApplicationInfo(
|
||||
"im.cwtch.flwtch", // Must be app name
|
||||
|
@ -154,12 +227,15 @@ class MainActivity: FlutterActivity() {
|
|||
// receives messages from the ForegroundService (which provides, ironically enough, the backend)
|
||||
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
var method = call.method
|
||||
val argmap: Map<String, String> = call.arguments as Map<String, String>
|
||||
// todo change usage patern to match that in FlwtchWorker
|
||||
// Unsafe for anything using int args, causes access time attempt to cast to string which will fail
|
||||
val argmap: Map<String, String> = call.arguments as Map<String,String>
|
||||
|
||||
// the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start()
|
||||
// in case the ForegroundService is still running. in both cases, however, we *do* want to re-register
|
||||
// the eventbus listener.
|
||||
if (call.method == "Start") {
|
||||
when (call.method) {
|
||||
"Start" -> {
|
||||
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
|
||||
|
||||
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get
|
||||
|
@ -177,10 +253,10 @@ class MainActivity: FlutterActivity() {
|
|||
// 15 minutes is the shortest interval you can request
|
||||
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
|
||||
return
|
||||
} else if (call.method == "CreateDownloadableFile") {
|
||||
}
|
||||
"CreateDownloadableFile" -> {
|
||||
this.dlToProfile = argmap["ProfileOnion"] ?: ""
|
||||
this.dlToHandle = argmap["handle"] ?: ""
|
||||
this.dlToHandle = call.argument("conversation")!!
|
||||
val suggestedName = argmap["filename"] ?: "filename.ext"
|
||||
this.dlToFileKey = argmap["filekey"] ?: ""
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
|
@ -189,8 +265,8 @@ class MainActivity: FlutterActivity() {
|
|||
putExtra(Intent.EXTRA_TITLE, suggestedName)
|
||||
}
|
||||
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
|
||||
return
|
||||
} else if (call.method == "ExportPreviewedFile") {
|
||||
}
|
||||
"ExportPreviewedFile" -> {
|
||||
this.exportFromPath = argmap["Path"] ?: ""
|
||||
val suggestion = argmap["FileName"] ?: "filename.ext"
|
||||
var imgType = "jpeg"
|
||||
|
@ -209,21 +285,288 @@ class MainActivity: FlutterActivity() {
|
|||
putExtra(Intent.EXTRA_TITLE, suggestion)
|
||||
}
|
||||
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
|
||||
}
|
||||
"ExportProfile" -> {
|
||||
|
||||
val profileOnion: String = call.argument("ProfileOnion") ?: ""
|
||||
val file: String = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(call.argument("file") ?: "").toString()
|
||||
Log.i("FlwtchWorker", "constructing exported file " + file);
|
||||
Cwtch.exportProfile(profileOnion,file)
|
||||
|
||||
this.exportFromPath = argmap["file"] ?: ""
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/gzip"
|
||||
putExtra(Intent.EXTRA_TITLE, argmap["file"])
|
||||
}
|
||||
startActivityForResult(intent, PROFILE_EXPORT_REQUEST_CODE)
|
||||
}
|
||||
"GetMessages" -> {
|
||||
Log.d("MainActivity.kt", "Cwtch GetMessages")
|
||||
|
||||
val profile = argmap["ProfileOnion"] ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val indexI: Int = call.argument("index") ?: 0
|
||||
val count: Int = call.argument("count") ?: 1
|
||||
|
||||
result.success(Cwtch.getMessages(profile, conversation.toLong(), indexI.toLong(), count.toLong()))
|
||||
return
|
||||
}
|
||||
"SendMessage" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val message: String = call.argument("message") ?: ""
|
||||
result.success(Cwtch.sendMessage(profile, conversation.toLong(), message))
|
||||
return
|
||||
}
|
||||
"SendInvitation" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val target: Int = call.argument("target") ?: 0
|
||||
result.success(Cwtch.sendInvitation(profile, conversation.toLong(), target.toLong()))
|
||||
return
|
||||
}
|
||||
|
||||
"ShareFile" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val filepath: String = call.argument("filepath") ?: ""
|
||||
result.success(Cwtch.shareFile(profile, conversation.toLong(), filepath))
|
||||
return
|
||||
}
|
||||
|
||||
"GetSharedFiles" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
result.success(Cwtch.getSharedFiles(profile, conversation.toLong()))
|
||||
return
|
||||
}
|
||||
|
||||
"RestartSharing" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val filepath: String = call.argument("filekey") ?: ""
|
||||
result.success(Cwtch.restartSharing(profile, filepath))
|
||||
return
|
||||
}
|
||||
|
||||
"StopSharing" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val filepath: String = call.argument("filekey") ?: ""
|
||||
result.success(Cwtch.stopSharing(profile, filepath))
|
||||
return
|
||||
}
|
||||
|
||||
"CreateProfile" -> {
|
||||
val nick: String = call.argument("nick") ?: ""
|
||||
val pass: String = call.argument("pass") ?: ""
|
||||
val autostart: Boolean = call.argument("autostart") ?: true
|
||||
Cwtch.createProfile(nick, pass, autostart)
|
||||
}
|
||||
"LoadProfiles" -> {
|
||||
val pass: String = call.argument("pass") ?: ""
|
||||
Cwtch.loadProfiles(pass)
|
||||
}
|
||||
"ActivatePeerEngine" -> {
|
||||
val profile: String = call.argument("profile") ?: ""
|
||||
Cwtch.activatePeerEngine(profile)
|
||||
}
|
||||
"DeactivatePeerEngine" -> {
|
||||
val profile: String = call.argument("profile") ?: ""
|
||||
Cwtch.deactivatePeerEngine(profile)
|
||||
}
|
||||
"ChangePassword" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val pass: String = call.argument("OldPass") ?: ""
|
||||
val passNew: String = call.argument("NewPass") ?: ""
|
||||
val passNew2: String = call.argument("NewPassAgain") ?: ""
|
||||
Cwtch.changePassword(profile, pass, passNew, passNew2)
|
||||
}
|
||||
"GetMessage" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val indexI: Int = call.argument("index") ?: 0
|
||||
result.success(Cwtch.getMessage(profile, conversation.toLong(), indexI.toLong()))
|
||||
return
|
||||
}
|
||||
"GetMessageByID" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val id: Int = call.argument("id") ?: 0
|
||||
result.success(Cwtch.getMessageByID(profile, conversation.toLong(), id.toLong()))
|
||||
return
|
||||
}
|
||||
"GetMessageByContentHash" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val contentHash: String = call.argument("contentHash") ?: ""
|
||||
result.success(Cwtch.getMessagesByContentHash(profile, conversation.toLong(), contentHash))
|
||||
return
|
||||
}
|
||||
"SetMessageAttribute" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val channel: Int = call.argument("Chanenl") ?: 0
|
||||
val midx: Int = call.argument("Message") ?: 0
|
||||
val key: String = call.argument("key") ?: ""
|
||||
val value: String = call.argument("value") ?: ""
|
||||
Cwtch.setMessageAttribute(profile, conversation.toLong(), channel.toLong(), midx.toLong(), key, value)
|
||||
}
|
||||
"AcceptConversation" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.acceptConversation(profile, conversation.toLong())
|
||||
}
|
||||
"BlockContact" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.blockContact(profile, conversation.toLong())
|
||||
}
|
||||
"UnblockContact" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.unblockContact(profile, conversation.toLong())
|
||||
}
|
||||
|
||||
"DownloadFile" -> {
|
||||
Log.d("MainActivity.kt", "Cwtch Download File Called...")
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val filepath: String = call.argument("filepath") ?: ""
|
||||
val manifestpath: String = call.argument("manifestpath") ?: ""
|
||||
val filekey: String = call.argument("filekey") ?: ""
|
||||
// FIXME: Prevent spurious calls by Intent
|
||||
if (profile != "") {
|
||||
Cwtch.downloadFile(profile, conversation.toLong(), filepath, manifestpath, filekey)
|
||||
}
|
||||
}
|
||||
"CheckDownloadStatus" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val fileKey: String = call.argument("fileKey") ?: ""
|
||||
Cwtch.checkDownloadStatus(profile, fileKey)
|
||||
}
|
||||
"VerifyOrResumeDownload" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val fileKey: String = call.argument("fileKey") ?: ""
|
||||
Cwtch.verifyOrResumeDownload(profile, conversation.toLong(), fileKey)
|
||||
}
|
||||
"SendProfileEvent" -> {
|
||||
val onion: String= call.argument("onion") ?: ""
|
||||
val jsonEvent: String = call.argument("jsonEvent") ?: ""
|
||||
Cwtch.sendProfileEvent(onion, jsonEvent)
|
||||
}
|
||||
"SendAppEvent" -> {
|
||||
val jsonEvent: String = call.argument("jsonEvent") ?: ""
|
||||
Cwtch.sendAppEvent(jsonEvent)
|
||||
}
|
||||
"ResetTor" -> {
|
||||
Cwtch.resetTor()
|
||||
}
|
||||
"ImportBundle" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val bundle: String = call.argument("bundle") ?: ""
|
||||
result.success(Cwtch.importBundle(profile, bundle))
|
||||
}
|
||||
"CreateGroup" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val server: String = call.argument("server") ?: ""
|
||||
val groupName: String = call.argument("groupName") ?: ""
|
||||
Cwtch.createGroup(profile, server, groupName)
|
||||
}
|
||||
"DeleteProfile" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val pass: String = call.argument("pass") ?: ""
|
||||
Cwtch.deleteProfile(profile, pass)
|
||||
}
|
||||
"ArchiveConversation" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.archiveConversation(profile, conversation.toLong())
|
||||
}
|
||||
"DeleteConversation" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.deleteContact(profile, conversation.toLong())
|
||||
}
|
||||
"SetProfileAttribute" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val key: String = call.argument("Key") ?: ""
|
||||
val v: String = call.argument("Val") ?: ""
|
||||
Cwtch.setProfileAttribute(profile, key, v)
|
||||
}
|
||||
"SetConversationAttribute" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val key: String = call.argument("Key") ?: ""
|
||||
val v: String = call.argument("Val") ?: ""
|
||||
Cwtch.setConversationAttribute(profile, conversation.toLong(), key, v)
|
||||
}
|
||||
"LoadServers" -> {
|
||||
val password: String = call.argument("Password") ?: ""
|
||||
Cwtch.loadServers(password)
|
||||
}
|
||||
"CreateServer" -> {
|
||||
val password: String = call.argument("Password") ?: ""
|
||||
val desc: String = call.argument("Description") ?: ""
|
||||
val autostart: Boolean = call.argument("Autostart") ?: false
|
||||
Cwtch.createServer(password, desc, autostart)
|
||||
}
|
||||
"DeleteServer" -> {
|
||||
val serverOnion: String = call.argument("ServerOnion") ?: ""
|
||||
val password: String = call.argument("Password") ?: ""
|
||||
Cwtch.deleteServer(serverOnion, password)
|
||||
}
|
||||
"LaunchServers" -> {
|
||||
Cwtch.launchServers()
|
||||
}
|
||||
"LaunchServer" -> {
|
||||
val serverOnion: String = call.argument("ServerOnion") ?: ""
|
||||
Cwtch.launchServer(serverOnion)
|
||||
}
|
||||
"StopServer" -> {
|
||||
val serverOnion: String = call.argument("ServerOnion") ?: ""
|
||||
Cwtch.stopServer(serverOnion)
|
||||
}
|
||||
"StopServers" -> {
|
||||
Cwtch.stopServers()
|
||||
}
|
||||
"DestroyServers" -> {
|
||||
Cwtch.destroyServers()
|
||||
}
|
||||
"SetServerAttribute" -> {
|
||||
val serverOnion: String = call.argument("ServerOnion") ?: ""
|
||||
val key: String = call.argument("Key") ?: ""
|
||||
val v: String = call.argument("Val") ?: ""
|
||||
Cwtch.setServerAttribute(serverOnion, key, v)
|
||||
}
|
||||
"ImportProfile" -> {
|
||||
val file: String = call.argument("file") ?: ""
|
||||
val pass: String = call.argument("pass") ?: ""
|
||||
Data.Builder().putString("result", Cwtch.importProfile(file, pass)).build()
|
||||
}
|
||||
"ReconnectCwtchForeground" -> {
|
||||
Cwtch.reconnectCwtchForeground()
|
||||
}
|
||||
"Shutdown" -> {
|
||||
Cwtch.shutdownCwtch();
|
||||
}
|
||||
else -> {
|
||||
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
|
||||
WorkManager.getInstance(this).enqueue(workRequest)
|
||||
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
|
||||
this, Observer { workInfo ->
|
||||
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
|
||||
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
|
||||
val res = workInfo.outputData.keyValueMap.toString()
|
||||
result.success(workInfo.outputData.getString("result"))
|
||||
}
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver
|
||||
|
@ -232,19 +575,22 @@ class MainActivity: FlutterActivity() {
|
|||
Log.i("MainActivity.kt", "onResume")
|
||||
if (myReceiver == null) {
|
||||
Log.i("MainActivity.kt", "onResume registering local broadcast receiver / event bus forwarder")
|
||||
val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS)
|
||||
val bm = flutterEngine?.dartExecutor?.binaryMessenger;
|
||||
if (bm != null) {
|
||||
val mc = MethodChannel(bm, CWTCH_EVENTBUS)
|
||||
|
||||
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS")
|
||||
myReceiver = MyBroadcastReceiver(mc)
|
||||
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(myReceiver!!, filter)
|
||||
LocalBroadcastManager.getInstance(applicationContext)
|
||||
.registerReceiver(myReceiver!!, filter)
|
||||
}
|
||||
}
|
||||
|
||||
// ReconnectCwtchForeground which will resync counters and settings...
|
||||
// We need to do this here because after a "pause" flutter is still running
|
||||
// but we might have lost sync with the background process...
|
||||
Log.i("MainActivity.kt", "Call ReconnectCwtchForeground")
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, "ReconnectCwtchForeground").putString(FlwtchWorker.KEY_ARGS, "{}").build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
|
||||
WorkManager.getInstance(applicationContext).enqueue(workRequest)
|
||||
Cwtch.reconnectCwtchForeground()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -256,6 +602,7 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.i("MainActivity.kt", "onDestroy - cancelling all WORKER_TAG and pruning old work")
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
app:lottie_autoPlay="true"
|
||||
app:lottie_rawRes="@raw/cwtch_animated_logo_op"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_speed="1.00" />
|
||||
app:lottie_speed="1.00"
|
||||
app:lottie_enableMergePathsForKitKatAndAbove="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,12 +1,13 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.5.31'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
// jCenter() no longer exists... https://blog.gradle.org/jcenter-shutdown
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.4'
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
}
|
||||
|
@ -15,7 +16,7 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
#Mon Jun 20 10:33:21 PDT 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
BIN
assets/knott.png
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
@ -0,0 +1,8 @@
|
|||
targets:
|
||||
$default:
|
||||
sources:
|
||||
- lib/**
|
||||
- pubspec.*
|
||||
- $package$
|
||||
# Allows the code generator to target files outside of the lib folder
|
||||
- integration_test/**.dart
|
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 381 KiB |
After Width: | Height: | Size: 204 KiB |
After Width: | Height: | Size: 346 KiB |
After Width: | Height: | Size: 177 KiB |
|
@ -0,0 +1,16 @@
|
|||
Cwtch (/kʊtʃ/ - a Welsh word roughly translating to “a hug that creates a safe place”) is a decentralized,
|
||||
privacy-preserving, multi-party messaging protocol that can be used to build metadata resistant applications.
|
||||
|
||||
- Decentralized and Open: There is no “Cwtch service” or “Cwtch network”. Participants in Cwtch
|
||||
can host their own safe spaces, or lend their infrastructure to others seeking a safe space.
|
||||
The Cwtch protocol is open, and anyone is free to build bots, services and user interfaces and
|
||||
integrate and interact with Cwtch.
|
||||
|
||||
- Privacy Preserving: All communication in Cwtch is end-to-end encrypted and takes place over Tor v3
|
||||
onion services.
|
||||
|
||||
- Metadata Resistant: Cwtch has been designed such that no information is exchanged or available to
|
||||
anyone without their explicit consent, including on-the-wire messages and protocol metadata.
|
||||
|
||||
For more information on how Cwtch works and a guide to metadata resistant communication please
|
||||
checkout the Cwtch Handbook: https://docs.cwtch.im/
|
|
@ -0,0 +1 @@
|
|||
Metadata resistant privacy platform designed to help you resist surveillance
|
|
@ -0,0 +1 @@
|
|||
Cwtch
|
|
@ -0,0 +1 @@
|
|||
https://cwtch.im/cwtch-explainer.mp4
|
|
@ -3,4 +3,6 @@
|
|||
VERSION=`cat LIBCWTCH-GO-MACOS.version`
|
||||
echo $VERSION
|
||||
|
||||
curl https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.dylib --output libCwtch.dylib
|
||||
curl --fail https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.x64.dylib --output libCwtch.x64.dylib
|
||||
curl --fail https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.arm64.dylib --output libCwtch.arm64.dylib
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
$Env:VERSION = type LIBCWTCH-GO.version
|
||||
echo $Env:VERSION
|
||||
|
||||
# This should automatically fail on error...
|
||||
Invoke-WebRequest -Uri https://build.openprivacy.ca/files/libCwtch-go-$Env:VERSION/libCwtch.dll -OutFile windows/libCwtch.dll
|
||||
|
||||
#Invoke-WebRequest -Uri https://build.openprivacy.ca/files/libCwtch-go-$Env:VERSION/cwtch.aar -OutFile android/cwtch/cwtch.aar
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
VERSION=`cat LIBCWTCH-GO.version`
|
||||
echo $VERSION
|
||||
|
||||
wget https://build.openprivacy.ca/files/libCwtch-go-$VERSION/cwtch.aar -O android/cwtch/cwtch.aar
|
||||
wget https://build.openprivacy.ca/files/libCwtch-go-$VERSION/libCwtch.so -O linux/libCwtch.so
|
||||
curl --fail https://build.openprivacy.ca/files/libCwtch-go-$VERSION/cwtch.aar --output android/cwtch/cwtch.aar
|
||||
curl --fail https://build.openprivacy.ca/files/libCwtch-go-$VERSION/libCwtch.so --output linux/libCwtch.so
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
cd macos
|
||||
curl https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-macos-0.4.6.7.tar.gz --output tor.tar.gz
|
||||
curl https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-macos-0.4.7.8.tar.gz --output tor.tar.gz
|
||||
tar -xzf tor.tar.gz
|
||||
chmod a+x Tor/tor.real
|
||||
cd ..
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip
|
||||
Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.7.8.zip -OutFile tor.zip
|
||||
|
||||
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }
|
||||
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '5b8f900a37f6e90d7a945b3903d769383c7478042cb43b2105d2374186e1a536f1a4758a2823d1d5be71d53a81dcfd8243293e04f82812d355983df322823cf4' ) { Write-Error 'tor.zip sha512sum mismatch' }
|
||||
|
||||
Expand-Archive -Path tor.zip -DestinationPath Tor
|
||||
|
|
10
fetch-tor.sh
|
@ -1,12 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.5.9-linux-x86_64 -O linux/tor
|
||||
chmod a+x linux/tor
|
||||
cd linux
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.7.8-linux-x86_64.tar.gz -O tor.tar.gz
|
||||
tar -xzf tor.tar.gz
|
||||
cd ..
|
||||
|
||||
mkdir -p android/app/src/main/jniLibs/arm64-v8a
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.4.9-arm64_pie -O android/app/src/main/jniLibs/arm64-v8a/libtor.so
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.7.10-arm64 -O android/app/src/main/jniLibs/arm64-v8a/libtor.so
|
||||
chmod a+x android/app/src/main/jniLibs/arm64-v8a/libtor.so
|
||||
|
||||
mkdir -p android/app/src/main/jniLibs/armeabi-v7a
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.4.9-arm_pie -O android/app/src/main/jniLibs/armeabi-v7a/libtor.so
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.7.10-arm7 -O android/app/src/main/jniLibs/armeabi-v7a/libtor.so
|
||||
chmod a+x android/app/src/main/jniLibs/armeabi-v7a/libtor.so
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
var fs = require("fs");
|
||||
var reporter = require('cucumber-html-reporter');
|
||||
const reportRootDir = 'integration_test/gherkin/reports/'
|
||||
const jsonReportPath = `${reportRootDir}json_report.json`;
|
||||
const htmlReportPath = `${reportRootDir}cucumber_report.html`;
|
||||
const reportFile = fs.readFileSync(`${reportRootDir}integration_response_data.json`);
|
||||
//const jsonReport = JSON.parse(JSON.parse(reportFile).gherkin_reports)[0];
|
||||
const jsonReport = JSON.parse(reportFile);
|
||||
fs.writeFileSync(jsonReportPath, JSON.stringify(jsonReport));
|
||||
|
||||
var options = {
|
||||
theme: 'bootstrap',
|
||||
jsonFile: jsonReportPath,
|
||||
output: htmlReportPath,
|
||||
reportSuiteAsScenarios: true,
|
||||
launchReport: false,
|
||||
};
|
||||
|
||||
reporter.generate(options);
|
|
@ -0,0 +1,60 @@
|
|||
## Environments
|
||||
|
||||
Located in the `integration_test/env` folder and managed by the hooks in `integration_test/hooks/env.dart`. Specify the environment you want a feature to run in by tagging it.
|
||||
|
||||
* `[no tag] (env/default)`: default environment to load if none is specified
|
||||
* `@env:aliceandbob1 (env/aliceandbob1)`: no-password Alice, Bob, and Carol profiles. Alice and Bob have already added each other, Carol has no contacts
|
||||
* `@env:persist (env/persist)`: changes made to this profile persist between features and scenarios (but NOT between runs)
|
||||
* `@env:clean`: runs the feature with no profile existing yet on disk
|
||||
|
||||
## Tests
|
||||
|
||||
[ ] 1. general
|
||||
[X] splash screen + clean load
|
||||
[X] setting save+load (TODO: dropdowns)
|
||||
[~] tor status+reset
|
||||
[~] shutdown cwtch
|
||||
[ ] 2. global settings (verify functionality)
|
||||
[_] language # blocked by dropdown
|
||||
[_] theme+color theme # blocked by dropdown
|
||||
[ ] column mode -> background? so all tests check both modes?
|
||||
[X] block unknown
|
||||
[X] streamer mode
|
||||
[ ] 3. experiments (
|
||||
[ ] group chat -> needs many
|
||||
[ ] server hosting -> also many
|
||||
[ ] file sharing -> a couple
|
||||
[ ] image previews
|
||||
[ ] clickable links (how much to test?)
|
||||
[ ] 4. profile mgmt
|
||||
[X] create+delete
|
||||
[X] default+password load
|
||||
[X] name change
|
||||
[ ] password change
|
||||
[ ] known server mgmt
|
||||
[ ] 5. p2p chat
|
||||
[ ] add, remove, block, archive
|
||||
[ ] invite accept+reject
|
||||
[X] send+receive
|
||||
[ ] acks
|
||||
[ ] try to send a long message
|
||||
[ ] malformed messages, replies
|
||||
[ ] overlays (invite, file/image)
|
||||
[ ] send
|
||||
[ ] receive
|
||||
[ ] functionality
|
||||
[ ] 6. p2p settings
|
||||
[ ] name saving + transmission
|
||||
[ ] block (ui indicators, functionality) inc in groups
|
||||
[ ] history save+load
|
||||
[ ] 7. groupchat
|
||||
[ ] add, leave, archive
|
||||
[ ] send+receive inc acks
|
||||
[ ] try to send a long message
|
||||
[ ] malformed messages, replies
|
||||
[ ] overlays (invite, file/image) inc from non-contacts
|
||||
[ ] send
|
||||
[ ] receive
|
||||
[ ] functionality
|
||||
[ ] 8. group settings
|
||||
[ ] display name
|
|
@ -1,53 +0,0 @@
|
|||
// This is a basic Flutter integration test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility that Flutter provides. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'package:cwtch/main_test.dart' as app;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
_testMain();
|
||||
}
|
||||
|
||||
void _testMain() {
|
||||
testWidgets('Blocked message rejection test', (WidgetTester tester) async {
|
||||
final String testerProfile = "mr roboto";
|
||||
final String blockedProfile = "rudey";
|
||||
|
||||
// start the app and render a few frames
|
||||
app.main();
|
||||
await tester.pump(); await tester.pump(); await tester.pump();
|
||||
//await tester.pumpAndSettle();
|
||||
|
||||
for (var i = 0; i < 30; i++) {
|
||||
print("$i pump");
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
// log in to a profile with a blocked contact
|
||||
await tester.tap(find.text(testerProfile));
|
||||
await tester.pump(); await tester.pump(); await tester.pump();
|
||||
expect(find.byIcon(Icons.block), findsOneWidget);
|
||||
|
||||
// use the debug control to inject a message from the contact
|
||||
await tester.tap(find.byIcon(Icons.bug_report));
|
||||
await tester.pump(); await tester.pump(); await tester.pump();
|
||||
|
||||
|
||||
// screenshot test
|
||||
print(Directory.current);
|
||||
//Directory.current = "/home/erinn/AndroidStudioProjects/flwtch/integration_test";
|
||||
await expectLater(find.byKey(Key('app')), matchesGoldenFile('blockedcontact.png'));
|
||||
// any active message badges?
|
||||
expect(find.text('1'), findsNothing);
|
||||
});
|
||||
}
|
Before Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1 @@
|
|||
_âeK%?Š!ţ~‡Lö9<C3B6>u×ÍlýQ’Q‚ż¦U•rMQCN5<4E>T-Ó/[<ń<ěn@KgŚă-ŕóŕČŃÓWÇ^l$řI‘C]»ÎI×7Đů@z¤m•Şb ŠNgířż?ő:†IşäD!ă±6ć°%čě…b
|
1
integration_test/env/aliceandbob1/dev/profiles/648b3aac5a139faadf74661983ab072e/SALT
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
¢‰qö3‰ ÉÌ¥êÒŽB7Å¢(Ê–vQBöÞɱ<C389>øŒœ¾F±zŠ\\UƒÈG[Ü/£Ñ?uš¼\;]y”›HþG|þÛ,Þ3xÛÞe‘E0!¬ÄSÍž<nÐÃòÐÉ®M~üw “ÀëQ@6Ǹ˒Öo£ÉüØ…ÕöÀi’ò
|
1
integration_test/env/aliceandbob1/dev/profiles/648b3aac5a139faadf74661983ab072e/VERSION
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
2
|
BIN
integration_test/env/aliceandbob1/dev/profiles/648b3aac5a139faadf74661983ab072e/db
vendored
Normal file
1
integration_test/env/aliceandbob1/dev/profiles/aef99a4db367d0a06f8a351f4732e2db/SALT
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
;KĄĂ”ČÓŠť\|ç<ÂŐÉ^1iRüÁw°ôFŔQĄ'¢©©z{P4ĂP(ä"5͸Qpr7˝`ŇK^uý¸ČÖ;©1&Ĺ,vŞ
K/YößžŹ‹mĄâ}±3›]/§v"&ĽiѸ!3Wîyëjuvą¶D+w_'
|
1
integration_test/env/aliceandbob1/dev/profiles/aef99a4db367d0a06f8a351f4732e2db/VERSION
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
2
|
BIN
integration_test/env/aliceandbob1/dev/profiles/aef99a4db367d0a06f8a351f4732e2db/db
vendored
Normal file
2
integration_test/env/aliceandbob1/dev/profiles/c39e0660a885b6000173ffea9d214b5c/SALT
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
¢ž»5‡Ä ô<03>m-J0újÕx ŽÙð•ÛjÙß“K×çøs³C=íà¾t¶-ÿD÷ñÇecàIXF`íI´
|
||||
÷³6
Vr×gp4ËBóäÞS¿E<C2BF>tv–ìíä1iù¢‡”}ûZóÈWMóŒöIÈ´»1þ‡KB||Å,¢fEž%<<3C>D
|
1
integration_test/env/aliceandbob1/dev/profiles/c39e0660a885b6000173ffea9d214b5c/VERSION
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
2
|
BIN
integration_test/env/aliceandbob1/dev/profiles/c39e0660a885b6000173ffea9d214b5c/db
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
ó„Ý g–Þd7èfª>ZrPòV`dB<(÷ ôÈW`½
7¾c´¬n•ËnŠ.ü¾s8lÿ·“*dZUmÊŠí&‹¸ÊhøEëö8mê’<1E>«Y ŸüñrÒý×W’²H%{¸iùFÃÎ<äÿ[0²Ñâ”\yÚø-¯R½L´¨ -'
|
|
@ -0,0 +1,2 @@
|
|||
DÍkHzöĺ’N(Ăĺ•őŐ`x<>ć—×ë#€ÍěĎ3—]Öc˛QŽ,Ą2_Ś3‰őťŞRC6~-zSÉĘ?JŽčĐ<>4 fřŔăhŢýüv˝F†8áBü1Q\˛"lHh5í§
<¦‹;$J3č"źúBamT5<Á4îě•Żcj™ÚŽśËgˇŹÉ›/<2F>‰vęĽnKÄßVG†Ű~YdŞG ÝrQPÁrôŕ<C3B4>l›jěăČ-Aľ‹‹Z’Ľy÷_Ľ¸˙˛M·ôÄĽĂ!AX^HR˘¦ç<>őX;ăŘ©Ô)@2šŔ”tŕÍŁä ý´[cdX®ĹĹ+ý,0wa}ß%<25><11>
|
||||
Ď9˘x[ÝČNP]Ĺ5o‘_‘M@Ą†™±!©•Ű0:IÄŮóv€ó;ź ÔĂP¨Öe?€˛Śip*‰{<ŐČ—´,RyMnď-‡gËťëĺÉfŹÚV›<56>Dá>łĐ;S8őĎ0l¦k
"ľÖöĘ1˝żA4ţ<11>vG6î€Uş^–"zÓ«H¸´e”S•<53>°u\čč=Ś5ńë¨Ů¤»]aď˝3ąďí©g`î-SŻŢŰ…ý#ăE÷× ¸=Śµ˘W,8đdŚĚä.˙ŽŇ‰=şPüÖ•Ąń_<C584>jäĎřĚvyěž,Ľî<˙xokV»`<60>vQ|Ć/©˙ťs˝0şę-tÖ-ßĂ[şa›=ýôßć<>ŕé÷µ×łëöNřmďV!qŘŹZ†Đ˘˛Ł ̲±saŢSúT‚ż€”ôîěuDąL<>-‘Ç"šŇíRşŁŰ<*'ófgߧČM~ŇŚ"sŐ<73>âl/xuĹ<řsÂÄ4÷~Ůf>yś§`¦NÝ{ľ¨b!Ey'ˇězęoZ!×<>\"´a(Ýp,PhŔrZ…Ő‹Mµ-…ë+ĐľÇƹɌťÎĘÝô\}ë~ł?sJL|ú©z‚˙·°IaXYh/áRüą2;vą“u§…ÉĽ<C389>P€(
|
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,12 @@
|
|||
@env:clean
|
||||
Feature: Splash screen displays and then closes
|
||||
Scenario: splash screen appears
|
||||
Then I expect the widget 'SplashView' to be present within 10 seconds
|
||||
Then I expect the widget 'ProfileManagerView' to be present within 10 seconds
|
||||
# first-run of cwtch creates expected files and folders
|
||||
Then I expect the folder 'integration_test/env/temp' to exist
|
||||
And I expect the folder 'integration_test/env/temp/dev' to exist
|
||||
And I expect the file 'integration_test/env/temp/dev/SALT' to exist
|
||||
And I expect the file 'integration_test/env/temp/dev/ui.globals' to exist
|
||||
And I expect the folder 'integration_test/env/temp/dev/tor' to exist
|
||||
And I expect the file 'integration_test/env/temp/dev/tor/torrc' to exist
|
|
@ -0,0 +1,50 @@
|
|||
@env:persist
|
||||
Feature: Settings pane opens and can save settings persistently
|
||||
Scenario: Open the Settings pane
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the 'OpenSettingsView' button
|
||||
And I wait until the text 'Cwtch Settings' is present
|
||||
And I take a screenshot
|
||||
|
||||
Scenario: Change every setting (except Language)
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Given I tap the 'OpenSettingsView' button
|
||||
And I wait until the text 'Use Light Themes' is present
|
||||
When I tap the widget that contains the text "Use Light Themes"
|
||||
And I tap the widget that contains the text "Block Unknown Contacts"
|
||||
And I tap the widget that contains the text "Streamer/Presentation Mode"
|
||||
And I tap the widget that contains the text "Enable Experiments"
|
||||
Then I wait until the text 'Enable Group Chat' is present
|
||||
And I tap the widget that contains the text "Enable Group Chat"
|
||||
And I tap the widget that contains the text "Hosting Servers"
|
||||
And I tap the widget that contains the text "File Sharing"
|
||||
Then I wait until the text 'Image Previews and Profile Pictures' is present
|
||||
And I tap the widget that contains the text "Image Previews and Profile Pictures"
|
||||
And I wait until the text 'Download Folder' is present
|
||||
And I fill the "DownloadFolderPicker" field with "/this/is/a/test"
|
||||
And I tap the widget that contains the text "Enable Clickable Links"
|
||||
Then I expect the switch that contains the text "Use Light Themes" to be checked
|
||||
And I expect the switch that contains the text "Block Unknown Contacts" to be checked
|
||||
And I expect the switch that contains the text "Streamer/Presentation Mode" to be checked
|
||||
And I expect the switch that contains the text "Enable Experiments" to be checked
|
||||
And I expect the switch that contains the text "Enable Group Chat" to be checked
|
||||
And I expect the switch that contains the text "Hosting Servers" to be checked
|
||||
And I expect the switch that contains the text "File Sharing" to be checked
|
||||
And I expect the switch that contains the text "Image Previews and Profile Pictures" to be checked
|
||||
And I expect the "DownloadFolderPicker" to be "/this/is/a/test"
|
||||
And I expect the switch that contains the text "Enable Clickable Links" to be checked
|
||||
|
||||
Scenario: When the app is reloaded, settings from the previous scenario have persisted
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Given I tap the 'OpenSettingsView' button
|
||||
And I wait until the text 'Use Light Themes' is present
|
||||
Then I expect the switch that contains the text "Use Light Themes" to be checked
|
||||
And I expect the switch that contains the text "Block Unknown Contacts" to be checked
|
||||
And I expect the switch that contains the text "Streamer/Presentation Mode" to be checked
|
||||
And I expect the switch that contains the text "Enable Experiments" to be checked
|
||||
And I expect the switch that contains the text "Enable Group Chat" to be checked
|
||||
And I expect the switch that contains the text "Hosting Servers" to be checked
|
||||
And I expect the switch that contains the text "File Sharing" to be checked
|
||||
And I expect the switch that contains the text "Image Previews and Profile Pictures" to be checked
|
||||
And I expect the "DownloadFolderPicker" to be "/this/is/a/test"
|
||||
And I expect the switch that contains the text "Enable Clickable Links" to be checked
|
|
@ -0,0 +1,14 @@
|
|||
Feature: Tor initializes correctly
|
||||
Scenario: Check the Tor version
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the icon with type "TorIcon"
|
||||
Then I expect the Tor version to be present
|
||||
And I expect the string 'Online' to be present within 60 seconds
|
||||
|
||||
Scenario: Reset Tor
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the icon with type "TorIcon"
|
||||
Then I expect the string 'Online' to be present within 60 seconds
|
||||
Then I tap the button that contains the text "Reset"
|
||||
And I wait for 1 second
|
||||
Then I expect the text "Online" to be absent
|
|
@ -0,0 +1,8 @@
|
|||
Feature: Shutdown Cwtch button works correctly
|
||||
Scenario: Clicking 'Shutdown Cwtch' shuts down Cwtch
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the button with tooltip 'Shutdown Cwtch'
|
||||
Then I expect the text 'Shutdown Cwtch?' to be present
|
||||
#this also kills the testing framework sadly. will have to find a workaround
|
||||
#And I tap the button that contains the text 'Shutdown Cwtch'
|
||||
#Then I wait until the widget with type 'ProfileMgrView' is absent
|
|
@ -0,0 +1,16 @@
|
|||
Feature: Global 'language' setting
|
||||
Scenario: Change the language to French and back
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Given I tap the 'OpenSettingsView' button
|
||||
And I wait until the text 'Language' is present
|
||||
Then I expect the text 'Language' to be present
|
||||
And I expect the text 'Langue' to be absent
|
||||
When I tap the widget that contains the text "English"
|
||||
And I wait until the text 'French' is present
|
||||
And I tap the widget that contains the text "French"
|
||||
And I wait until the text 'Langue' is present
|
||||
And I expect the text 'Language' to be absent
|
||||
When I tap the widget that contains the text "Français"
|
||||
And I tap the widget that contains the text "Anglais"
|
||||
And I wait until the text 'Language' is present
|
||||
And I expect the text 'Langue' to be absent
|
|
@ -0,0 +1,12 @@
|
|||
Feature: Global 'Theme' setting
|
||||
Scenario: Change the theme to Mermaid
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Given I tap the 'OpenSettingsView' button
|
||||
And I wait for 1 second
|
||||
When I tap the "DropdownTheme" button
|
||||
And I tap the element that contains the text "Mermaid"
|
||||
Scenario: Change the theme to Light Mode
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Given I tap the 'OpenSettingsView' button
|
||||
And I wait for 1 second
|
||||
And I tap the widget that contains the text "Theme"
|
|
@ -0,0 +1,20 @@
|
|||
@env:aliceandbob1
|
||||
Feature: Block unknown contacts setting
|
||||
Scenario: Carol adds Alice but Alice doesn't see it because Block Unknowns is enabled
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Given I tap the 'OpenSettingsView' button
|
||||
When I tap the widget that contains the text "Block Unknown Contacts"
|
||||
Then I expect the switch that contains the text "Block Unknown Contacts" to be checked
|
||||
Given I tap the back button
|
||||
And I wait until the text "Carol" is present
|
||||
And I tap the button that contains the text "Carol"
|
||||
And I tap the button with tooltip "Add a new contact or conversation"
|
||||
When I fill the "txtAddP2P" field with "vbmmsbx3rhndpfz6t3jkrd7m3yu62xzrldxkdgsw4rsehiwuw3tmo7yd"
|
||||
And I wait for 1 second
|
||||
And I take a screenshot
|
||||
And I tap the back button
|
||||
And I wait until the text "Alice" is present
|
||||
And I wait until the tooltip "Online" is present
|
||||
And I tap the button that contains the text "Alice"
|
||||
And I wait for 20 seconds
|
||||
Then I expect the text "yxj2pvhozedflp4g7yitpqkeho63maaffi2qgsj3e6s2fbmosuuas2qd" to be absent
|
|
@ -0,0 +1,18 @@
|
|||
@env:aliceandbob1
|
||||
Feature: Streamer mode
|
||||
Scenario: All onions disappear when Streamer Mode is enabled
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I wait until the text "vbmmsbx3rhndpfz6t3jkrd7m3yu62xzrldxkdgsw4rsehiwuw3tmo7yd" is present
|
||||
And I wait until the text "pjurzypqui3dnpxj6aemk6cqz22yx6zfr5lq4jzu7muwe2yyx2zrnzyd" is present
|
||||
Given I tap the 'OpenSettingsView' button
|
||||
And I wait for 1 second
|
||||
And I tap the widget that contains the text "Streamer/Presentation Mode"
|
||||
Then I expect the switch that contains the text "Streamer/Presentation Mode" to be checked
|
||||
When I tap the back button
|
||||
And I wait until the text "Alice" is present
|
||||
And I wait until the text "Bob" is present
|
||||
Then I expect the text "vbmmsbx3rhndpfz6t3jkrd7m3yu62xzrldxkdgsw4rsehiwuw3tmo7yd" to be absent
|
||||
And I expect the text "pjurzypqui3dnpxj6aemk6cqz22yx6zfr5lq4jzu7muwe2yyx2zrnzyd" to be absent
|
||||
When I tap the button that contains the text "Alice"
|
||||
Then I expect the text "vbmmsbx3rhndpfz6t3jkrd7m3yu62xzrldxkdgsw4rsehiwuw3tmo7yd" to be absent
|
||||
And I expect the text "pjurzypqui3dnpxj6aemk6cqz22yx6zfr5lq4jzu7muwe2yyx2zrnzyd" to be absent
|
|
@ -0,0 +1,90 @@
|
|||
@env:persist
|
||||
Feature: Basic Profile Management
|
||||
Scenario: Error on Creating a Profile without a Display Name
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the button with tooltip "Add new profile"
|
||||
Then I expect the text 'Display Name' to be present
|
||||
And I expect the text 'New Password' to be present
|
||||
And I expect the text 'Please enter a display name' to be absent
|
||||
Then I tap the "button" widget with label "Add new profile"
|
||||
And I expect the text 'Please enter a display name' to be present
|
||||
And I take a screenshot
|
||||
|
||||
Scenario: Create Unencrypted Profile
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the button with tooltip "Add new profile"
|
||||
Then I expect the text 'Display Name' to be present
|
||||
And I expect the text 'New Password' to be present
|
||||
And I take a screenshot
|
||||
Then I tap the "passwordCheckBox" widget
|
||||
And I expect the text 'New Password' to be absent
|
||||
And I take a screenshot
|
||||
Then I fill the "displayNameFormElement" field with "Alice (Unencrypted)"
|
||||
Then I tap the "button" widget with label "Add new profile"
|
||||
And I expect a "ProfileRow" widget with text "Alice (Unencrypted)"
|
||||
And I take a screenshot
|
||||
Then I tap the "ProfileRow" widget with label "Alice (Unencrypted)"
|
||||
And I expect the text "Alice (Unencrypted) » Conversations" to be present
|
||||
And I take a screenshot
|
||||
|
||||
Scenario: Load Unencrypted Profile
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Then I expect a "ProfileRow" widget with text "Alice (Unencrypted)"
|
||||
|
||||
Scenario: Create Encrypted Profile
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the button with tooltip "Add new profile"
|
||||
Then I expect the text 'Display Name' to be present
|
||||
And I expect the text 'New Password' to be present
|
||||
And I take a screenshot
|
||||
Then I fill the "displayNameFormElement" field with "Alice (Encrypted)"
|
||||
Then I fill the "passwordFormElement" field with "password1"
|
||||
Then I fill the "confirmPasswordFormElement" field with "password1"
|
||||
And I take a screenshot
|
||||
Then I tap the "button" widget with label "Add new profile"
|
||||
And I expect a "ProfileRow" widget with text "Alice (Encrypted)"
|
||||
And I take a screenshot
|
||||
Then I tap the "ProfileRow" widget with label "Alice (Encrypted)"
|
||||
And I expect the text 'Alice (Encrypted) » Conversations' to be present
|
||||
And I take a screenshot
|
||||
|
||||
Scenario: Load an Encrypted Profile by Unlocking it with a Password
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Then I expect the text 'Enter a password to view your profiles' to be absent
|
||||
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
|
||||
Then I expect the text 'Enter a password to view your profiles' to be present
|
||||
When I fill the "unlockPasswordProfileElement" field with "password1"
|
||||
And I tap the "button" widget with label "Unlock"
|
||||
Then I expect a "ProfileRow" widget with text "Alice (Encrypted)"
|
||||
|
||||
Scenario: Load an Encrypted Profile by Unlocking it with a Password and Change the Name
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Then I expect the text 'Enter a password to view your profiles' to be absent
|
||||
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
|
||||
Then I expect the text 'Enter a password to view your profiles' to be present
|
||||
When I fill the "unlockPasswordProfileElement" field with "password1"
|
||||
And I tap the "button" widget with label "Unlock"
|
||||
Then I expect a "ProfileRow" widget with text "Alice (Encrypted)"
|
||||
When I tap the "IconButton" widget with tooltip "Edit Profile Alice (Encrypted)"
|
||||
Then I expect the text 'Display Name' to be present
|
||||
Then I fill the "displayNameFormElement" field with "Carol (Encrypted)"
|
||||
And I tap the "button" widget with label "Save Profile"
|
||||
And I wait until the widget with type 'ProfileMgrView' is present
|
||||
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)"
|
||||
|
||||
Scenario: Delete an Encrypted Profile
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
Then I expect the text 'Enter a password to view your profiles' to be absent
|
||||
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
|
||||
Then I expect the text 'Enter a password to view your profiles' to be present
|
||||
When I fill the "unlockPasswordProfileElement" field with "password1"
|
||||
And I tap the "button" widget with label "Unlock"
|
||||
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)"
|
||||
And I take a screenshot
|
||||
When I tap the "IconButton" widget with tooltip "Edit Profile Carol (Encrypted)"
|
||||
Then I expect the text 'Display Name' to be present
|
||||
When I tap the button that contains the text "Delete"
|
||||
Then I expect the text "Really Delete Profile" to be present
|
||||
When I tap the "button" widget with label "Really Delete Profile"
|
||||
And I wait until the widget with type 'ProfileMgrView' is present
|
||||
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)" to be absent
|
|
@ -0,0 +1,31 @@
|
|||
@env:aliceandbob1
|
||||
Feature: Sending and receiving chat messages
|
||||
Background:
|
||||
Given I wait until the widget with type "ProfileRow" is present
|
||||
And I wait for 4 seconds
|
||||
Given I tap the button that contains the text "Alice"
|
||||
And I tap the button that contains the text "Bob"
|
||||
And I wait until the text "Contact is offline, messages can't be delivered right now" is absent
|
||||
#And I wait for 6 seconds
|
||||
When I fill the "txtCompose" field with "hello! this is a test!"
|
||||
And I tap the "btnSend" button
|
||||
Then I expect a "MessageBubble" widget with text "hello! this is a test!\u202F" to be present within 5 seconds
|
||||
#Then I expect the text "hello! this is a test!" to be present
|
||||
And I tap the back button
|
||||
And I tap the back button
|
||||
|
||||
Scenario: Bob receives the message from Alice
|
||||
Given I tap the button that contains the text "Bob"
|
||||
And I tap the button that contains the text "Alice"
|
||||
Then I expect a "MessageBubble" widget with text "hello! this is a test!\u202F" to be present within 5 seconds
|
||||
|
||||
Scenario: Bob replies to a message from Alice
|
||||
Given I tap the button that contains the text "Bob"
|
||||
And I tap the button that contains the text "Alice"
|
||||
#When I swipe right by 15 pixels on the element that contains the text "hello! this is a test!\u202F"
|
||||
#When I swipe right by 15 pixels on the widget of type "MessageBubble" with text "hello! this is a test!\u202F"
|
||||
And I tap the button with tooltip "Reply to this message"
|
||||
And I fill the "txtCompose" field with "yay the test worked"
|
||||
And I tap the "btnSend" button
|
||||
Then I expect to see the message "yay the test worked\u202F" replying to "hello! this is a test!" within 5 seconds
|
||||
And I take a screenshot
|
|
@ -0,0 +1,107 @@
|
|||
//import 'package:flutter_gherkin/flutter_gherkin_integration_test.dart'; // notice new import name
|
||||
import 'package:flutter_gherkin/flutter_gherkin.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gherkin/gherkin.dart';
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
// The application under test.
|
||||
import 'package:cwtch/main.dart' as app;
|
||||
import 'package:glob/glob.dart';
|
||||
|
||||
import 'gherkin_suite_test.dart';
|
||||
import 'hooks/env.dart';
|
||||
import 'steps/chat.dart';
|
||||
import 'steps/files.dart';
|
||||
import 'steps/form_elements.dart';
|
||||
import 'steps/overrides.dart';
|
||||
import 'steps/text.dart';
|
||||
import 'steps/utils.dart';
|
||||
|
||||
part 'gherkin_suite_test.g.dart';
|
||||
|
||||
const REPLACED_BY_SCRIPT = <String>['integration_test/features/**.feature'];
|
||||
|
||||
@GherkinTestSuite(executionOrder: ExecutionOrder.alphabetical, featurePaths: REPLACED_BY_SCRIPT)
|
||||
void main() async {
|
||||
final params = [
|
||||
SwitchStateParameter(),
|
||||
];
|
||||
|
||||
final steps = [
|
||||
// chat elements
|
||||
ExpectReply(),
|
||||
// form elements
|
||||
CheckSwitchState(),
|
||||
CheckSwitchStateWithText(),
|
||||
DropdownChoose(),
|
||||
// utils
|
||||
TakeScreenshot(),
|
||||
// overrides
|
||||
TapWidgetWithType(),
|
||||
TapWidgetWithLabel(),
|
||||
TapWidgetWithTooltip(),
|
||||
ExpectWidgetWithText(),
|
||||
AbsentWidgetWithText(),
|
||||
WaitUntilTypeExists(),
|
||||
ExpectTextToBePresent(),
|
||||
ExpectWidgetWithTextWithin(),
|
||||
WaitUntilTextExists(),
|
||||
WaitUntilTooltipExists(),
|
||||
SwipeOnType(),
|
||||
// text
|
||||
TorVersionPresent(),
|
||||
TooltipTap(),
|
||||
// files
|
||||
FolderExists(),
|
||||
FileExists(),
|
||||
];
|
||||
|
||||
var sb = StringBuffer();
|
||||
sb
|
||||
..writeln("## Custom Parameters\n")
|
||||
..writeln("| name | pattern |")
|
||||
..writeln("| --- | --- |");
|
||||
for (var i in params) {
|
||||
sb
|
||||
..write("| ")
|
||||
..write(i.identifier)
|
||||
..write(" | ")
|
||||
..write(i.pattern.toString().replaceFirst("RegExp: pattern=", "").replaceFirst(" flags=i", "").replaceAll("|", "|"))
|
||||
..writeln(" |");
|
||||
}
|
||||
sb
|
||||
..writeln("\n## Custom steps\n")
|
||||
..writeln("| pattern |")
|
||||
..writeln("| --- |");
|
||||
for (var i in steps) {
|
||||
sb.writeln(i.pattern.toString().replaceFirst("RegExp: pattern=", "| ").replaceFirst(" flags=", " |").replaceAll("|", "|"));
|
||||
}
|
||||
var f = File("integration_test/CustomSteps.md");
|
||||
f.writeAsString(sb.toString());
|
||||
|
||||
await executeTestSuite(
|
||||
configuration: FlutterTestConfiguration(
|
||||
reporters: [
|
||||
StdoutReporter(MessageLevel.verbose)
|
||||
..setWriteLineFn(print)
|
||||
..setWriteFn(print),
|
||||
ProgressReporter()
|
||||
..setWriteLineFn(print)
|
||||
..setWriteFn(print),
|
||||
TestRunSummaryReporter()
|
||||
..setWriteLineFn(print)
|
||||
..setWriteFn(print),
|
||||
JsonReporter(),
|
||||
],
|
||||
customStepParameterDefinitions: [
|
||||
SwitchStateParameter(),
|
||||
],
|
||||
stepDefinitions: steps,
|
||||
hooks: [
|
||||
ResetCwtchEnvironment(),
|
||||
AttachScreenshotOnFailedStepHook(),
|
||||
]),
|
||||
appMainFunction: (World world) => app.main(),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:gherkin/gherkin.dart';
|
||||
|
||||
class ResetCwtchEnvironment extends Hook {
|
||||
@override
|
||||
int get priority => 10;
|
||||
|
||||
@override
|
||||
Future<void> onBeforeRun(TestConfiguration config) async {
|
||||
// initialize @env:persist
|
||||
await Process.run("rm", ["-rf", "integration_test/env/temp-persist"]);
|
||||
await Process.run("rm", ["-rf", "integration_test/env/temp"]);
|
||||
await Process.run("cp", ["-R", "integration_test/env/persist", "integration_test/env/temp-persist"]);
|
||||
|
||||
return super.onBeforeRun(config);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onAfterRun(TestConfiguration config) async {
|
||||
// Clean up After a Test Run...
|
||||
print("clean up environments after run...");
|
||||
await Process.run("rm", ["-rf", "integration_test/env/temp-persist"]);
|
||||
await Process.run("rm", ["-rf", "integration_test/env/temp"]);
|
||||
return super.onAfterRun(config);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBeforeScenario(TestConfiguration config, String scenario, Iterable<Tag> tags) async {
|
||||
if (tags.any((t) => t.name == "@env:persist")) {
|
||||
await Process.run("mv", ["integration_test/env/temp-persist", "integration_test/env/temp"]);
|
||||
} else if (tags.any((t) => t.name == "@env:aliceandbob1")) {
|
||||
await Process.run("cp", ["-R", "integration_test/env/aliceandbob1", "integration_test/env/temp"]);
|
||||
} else if (!(tags.any((t) => t.name == "@env:clean"))) {
|
||||
// use the default environment if no @env: tag specified
|
||||
await Process.run("cp", ["-R", "integration_test/env/default", "integration_test/env/temp"]);
|
||||
} else {
|
||||
print("clean environment initialized");
|
||||
}
|
||||
return super.onBeforeScenario(config, scenario, tags);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onAfterScenario(TestConfiguration config, String scenario, Iterable<Tag> tags, {bool passed = true}) async {
|
||||
if (tags.any((t) => t.name == "@env:persist")) {
|
||||
await Process.run("mv", ["integration_test/env/temp", "integration_test/env/temp-persist"]);
|
||||
}
|
||||
return super.onAfterScenario(config, scenario, tags);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import 'package:cwtch/main.dart';
|
||||
import 'package:cwtch/widgets/messagebubble.dart';
|
||||
import 'package:cwtch/widgets/profilerow.dart';
|
||||
import 'package:cwtch/widgets/quotedmessage.dart';
|
||||
import 'package:cwtch/widgets/tor_icon.dart';
|
||||
import 'package:cwtch/views/profilemgrview.dart';
|
||||
import 'package:flutter_gherkin/flutter_gherkin.dart';
|
||||
import 'package:flutter_gherkin/src/flutter/parameters/existence_parameter.dart';
|
||||
import 'package:flutter_gherkin/src/flutter/parameters/swipe_direction_parameter.dart';
|
||||
import 'package:gherkin/gherkin.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'overrides.dart';
|
||||
|
||||
StepDefinitionGeneric ExpectReply() {
|
||||
return given3<String, String, int, FlutterWorld>(
|
||||
RegExp(r'I expect to see the message {string} replying to {string} within {int} second(s)$'),
|
||||
(originalMessage, responseMessage, seconds, context) async {
|
||||
await context.world.appDriver.waitUntil(
|
||||
() async {
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
|
||||
return await context.world.appDriver.isPresent(
|
||||
context.world.appDriver.findByDescendant(context.world.appDriver.findBy(QuotedMessageBubble, FindType.type), context.world.appDriver.findBy(originalMessage, FindType.text))) &&
|
||||
await context.world.appDriver.isPresent(
|
||||
context.world.appDriver.findByDescendant(context.world.appDriver.findBy(QuotedMessageBubble, FindType.type), context.world.appDriver.findBy(responseMessage, FindType.text)));
|
||||
},
|
||||
timeout: Duration(seconds: seconds),
|
||||
);
|
||||
},
|
||||
configuration: StepDefinitionConfiguration()..timeout = const Duration(days: 1),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter_gherkin/flutter_gherkin.dart';
|
||||
import 'package:gherkin/gherkin.dart';
|
||||
|
||||
StepDefinitionGeneric FolderExists() {
|
||||
return then1<String, FlutterWorld>(
|
||||
RegExp(r'I expect the folder {string} to exist'),
|
||||
(input1, context) async {
|
||||
context.expect(Directory(input1).existsSync(), true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric FileExists() {
|
||||
return then1<String, FlutterWorld>(
|
||||
RegExp(r'I expect the file {string} to exist'),
|
||||
(input1, context) async {
|
||||
context.expect(File(input1).existsSync(), true);
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_driver/flutter_driver.dart';
|
||||
import 'package:flutter_gherkin/flutter_gherkin.dart';
|
||||
import 'package:gherkin/gherkin.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
enum SwitchState { checked, unchecked }
|
||||
|
||||
class SwitchStateParameter extends CustomParameter<SwitchState> {
|
||||
SwitchStateParameter()
|
||||
: super("toggle", RegExp(r"(checked|unchecked)", caseSensitive: false), (s) {
|
||||
switch (s.toLowerCase()) {
|
||||
case "checked":
|
||||
return SwitchState.checked;
|
||||
case "unchecked":
|
||||
return SwitchState.unchecked;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class CheckSwitchState extends Given2WithWorld<String, SwitchState, FlutterWorld> {
|
||||
@override
|
||||
Future<void> executeStep(String input1, SwitchState state) async {
|
||||
final switch1 = world.appDriver.findBy(input1, FindType.key);
|
||||
bool switch1exists = await world.appDriver.isPresent(switch1);
|
||||
expect(switch1exists, true);
|
||||
if (switch1exists) {
|
||||
SwitchListTile wdgt = await world.appDriver.widget(switch1);
|
||||
expect(wdgt.value, state == SwitchState.checked);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
RegExp get pattern => RegExp(r"I expect the {string} widget to be {toggle}");
|
||||
}
|
||||
|
||||
StepDefinitionGeneric CheckSwitchStateWithText() {
|
||||
return then2<String, SwitchState, FlutterWorld>(
|
||||
RegExp(r'I expect the switch that contains the text {string} to be {toggle}'),
|
||||
(input1, state, context) async {
|
||||
final textFinder = context.world.appDriver.findBy(input1, FindType.text);
|
||||
await context.world.appDriver.scrollIntoView(textFinder);
|
||||
final switchTypeFinder = context.world.appDriver.findBy(SwitchListTile, FindType.type);
|
||||
final switchFinder = context.world.appDriver.findByAncestor(textFinder, switchTypeFinder);
|
||||
SwitchListTile switchWidget = await context.world.appDriver.widget(switchFinder);
|
||||
context.expect(switchWidget.value, state == SwitchState.checked);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric DropdownChoose() {
|
||||
return then2<int, String, FlutterWorld>(
|
||||
RegExp(r'I choose option {int} from the {string} dropdown'),
|
||||
(idx, input1, context) async {
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
final ddFinder = context.world.appDriver.findBy(input1, FindType.key);
|
||||
await context.world.appDriver.scrollIntoView(ddFinder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
await context.world.appDriver.tap(ddFinder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
|
||||
// somewhat complicated due to widget structure... we need to:
|
||||
// find [ancestor of type DropdownMenuItem] of [[Text with value <text of element #idx>] contained within Dropdown]
|
||||
DropdownButton ddWidget = await context.world.appDriver.widget(ddFinder);
|
||||
DropdownMenuItem itemWidget = ddWidget.items!.elementAt(idx);
|
||||
final itemText = (itemWidget.child as Text).data.toString();
|
||||
final textFinder = context.world.appDriver.findBy(itemText, FindType.text);
|
||||
final textWithinFinder = context.world.appDriver.findByDescendant(ddFinder, textFinder);
|
||||
final ddiFinder = context.world.appDriver.findBy(DropdownMenuItem<String>, FindType.type);
|
||||
//final ddiFinder = context.world.appDriver.findBy(_MenuItem, FindType.type);
|
||||
final itemFinder = context.world.appDriver.findByAncestor(textWithinFinder, ddiFinder, firstMatchOnly: true);
|
||||
await context.world.appDriver.tap(itemFinder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
// this file contains steps from flutter_gherkin with bugfixes/adaptations to our codebase
|
||||
|
||||
import 'package:cwtch/main.dart';
|
||||
import 'package:cwtch/widgets/messagebubble.dart';
|
||||
import 'package:cwtch/widgets/profilerow.dart';
|
||||
import 'package:cwtch/widgets/tor_icon.dart';
|
||||
import 'package:cwtch/views/profilemgrview.dart';
|
||||
import 'package:flutter_gherkin/flutter_gherkin.dart';
|
||||
import 'package:flutter_gherkin/src/flutter/parameters/existence_parameter.dart';
|
||||
import 'package:flutter_gherkin/src/flutter/parameters/swipe_direction_parameter.dart';
|
||||
import 'package:gherkin/gherkin.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
StepDefinitionGeneric TapWidgetWithType() {
|
||||
return given1<String, FlutterWorld>(
|
||||
RegExp(r'I tap the (?:button|element|label|icon|field|text|widget) with type {string}$'),
|
||||
(input1, context) async {
|
||||
await context.world.appDriver.tap(
|
||||
context.world.appDriver.findBy(
|
||||
widgetTypeByName(input1),
|
||||
FindType.type,
|
||||
),
|
||||
);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric TapWidgetWithLabel() {
|
||||
return given2<String, String, FlutterWorld>(
|
||||
RegExp(r'I tap the {string} widget with label {string}$'),
|
||||
(ofType, text, context) async {
|
||||
final finder =
|
||||
context.world.appDriver.findByDescendant(context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type), context.world.appDriver.findBy(text, FindType.text), firstMatchOnly: true);
|
||||
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
|
||||
//print(wdg.debugDescribeChildren().first.)
|
||||
await context.world.appDriver.scrollIntoView(finder);
|
||||
await context.world.appDriver.tap(finder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric TapWidgetWithTooltip() {
|
||||
return given2<String, String, FlutterWorld>(
|
||||
RegExp(r'I tap the {string} widget with tooltip {string}$'),
|
||||
(ofType, text, context) async {
|
||||
final finder = context.world.appDriver
|
||||
.findByDescendant(context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type), context.world.appDriver.findBy(text, FindType.tooltip), firstMatchOnly: true);
|
||||
await context.world.appDriver.scrollIntoView(finder);
|
||||
await context.world.appDriver.tap(finder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric ExpectWidgetWithText() {
|
||||
return given2<String, String, FlutterWorld>(
|
||||
RegExp(r'I expect a {string} widget with text {string}$'),
|
||||
(ofType, text, context) async {
|
||||
final finder =
|
||||
context.world.appDriver.findByDescendant(context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type), context.world.appDriver.findBy(text, FindType.text), firstMatchOnly: true);
|
||||
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
|
||||
//print(wdg.debugDescribeChildren().first.)
|
||||
await context.world.appDriver.isPresent(finder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric AbsentWidgetWithText() {
|
||||
return given2<String, String, FlutterWorld>(
|
||||
RegExp(r'I expect a {string} widget with text {string} to be absent$'),
|
||||
(ofType, text, context) async {
|
||||
final finder =
|
||||
context.world.appDriver.findByDescendant(context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type), context.world.appDriver.findBy(text, FindType.text), firstMatchOnly: true);
|
||||
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
|
||||
//print(wdg.debugDescribeChildren().first.)
|
||||
await context.world.appDriver.isAbsent(finder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric TapButtonWithText() {
|
||||
return given1<String, FlutterWorld>(
|
||||
RegExp(r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'),
|
||||
(input1, context) async {
|
||||
final finder = context.world.appDriver.findByDescendant(context.world.appDriver.findBy(Flwtch, FindType.type), context.world.appDriver.findBy(input1, FindType.key), firstMatchOnly: true);
|
||||
await context.world.appDriver.scrollIntoView(finder);
|
||||
await context.world.appDriver.tap(finder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric WaitUntilTypeExists() {
|
||||
return then2<String, Existence, FlutterWorld>(
|
||||
'I wait until the (?:button|element|label|icon|field|text|widget) with type {string} is {existence}',
|
||||
(ofType, existence, context) async {
|
||||
await context.world.appDriver.waitUntil(
|
||||
() async {
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
|
||||
return existence == Existence.absent
|
||||
? context.world.appDriver.isAbsent(
|
||||
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
|
||||
)
|
||||
: context.world.appDriver.isPresent(
|
||||
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric ExpectTextToBePresent() {
|
||||
return given2<String, int, FlutterWorld>(
|
||||
RegExp(r'I expect the string {string} to be present within {int} second(s)$'),
|
||||
(key, seconds, context) async {
|
||||
await context.world.appDriver.waitUntil(
|
||||
() async {
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
|
||||
return context.world.appDriver.isPresent(
|
||||
context.world.appDriver.findBy(key, FindType.text),
|
||||
);
|
||||
},
|
||||
timeout: Duration(seconds: seconds),
|
||||
);
|
||||
},
|
||||
configuration: StepDefinitionConfiguration()..timeout = const Duration(days: 1),
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric ExpectWidgetWithTextWithin() {
|
||||
return given3<String, String, int, FlutterWorld>(
|
||||
RegExp(r'I expect a {string} widget with text {string} to be present within {int} second(s)$'),
|
||||
(widgetType, text, seconds, context) async {
|
||||
await () async {
|
||||
var result = false;
|
||||
while (!result) {
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
|
||||
result = await context.world.appDriver.isPresent(
|
||||
context.world.appDriver.findByDescendant(context.world.appDriver.findBy(widgetTypeByName(widgetType), FindType.type), context.world.appDriver.findBy(text, FindType.text)),
|
||||
);
|
||||
}
|
||||
}()
|
||||
.timeout(Duration(seconds: 120));
|
||||
},
|
||||
configuration: StepDefinitionConfiguration()..timeout = const Duration(days: 1),
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric WaitUntilTextExists() {
|
||||
return then2<String, Existence, FlutterWorld>(
|
||||
'I wait until the text {string} is {existence}',
|
||||
(text, existence, context) async {
|
||||
await () async {
|
||||
var result = false;
|
||||
while (!result) {
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
|
||||
result = await (existence == Existence.absent
|
||||
? context.world.appDriver.isAbsent(
|
||||
context.world.appDriver.findBy(text, FindType.text),
|
||||
)
|
||||
: context.world.appDriver.isPresent(
|
||||
context.world.appDriver.findBy(text, FindType.text),
|
||||
));
|
||||
}
|
||||
}()
|
||||
.timeout(Duration(seconds: 120));
|
||||
},
|
||||
configuration: StepDefinitionConfiguration()..timeout = const Duration(days: 1),
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric WaitUntilTooltipExists() {
|
||||
return then2<String, Existence, FlutterWorld>(
|
||||
'I wait until the tooltip {string} is {existence}',
|
||||
(ofType, existence, context) async {
|
||||
await context.world.appDriver.waitUntil(
|
||||
() async {
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
|
||||
return existence == Existence.absent
|
||||
? context.world.appDriver.isAbsent(
|
||||
context.world.appDriver.findBy(ofType, FindType.tooltip),
|
||||
)
|
||||
: context.world.appDriver.isPresent(
|
||||
context.world.appDriver.findBy(ofType, FindType.tooltip),
|
||||
);
|
||||
},
|
||||
timeout: Duration(seconds: 120),
|
||||
);
|
||||
},
|
||||
configuration: StepDefinitionConfiguration()..timeout = const Duration(days: 1),
|
||||
);
|
||||
}
|
||||
|
||||
mixin _SwipeHelper on When4WithWorld<SwipeDirection, int, String, String, FlutterWorld> {
|
||||
Future<void> swipeOnFinder(
|
||||
dynamic finder,
|
||||
SwipeDirection direction,
|
||||
int swipeAmount,
|
||||
) async {
|
||||
if (direction == SwipeDirection.left || direction == SwipeDirection.right) {
|
||||
final offset = direction == SwipeDirection.right ? swipeAmount : (swipeAmount * -1);
|
||||
await world.appDriver.scroll(
|
||||
finder,
|
||||
dx: offset.toDouble(),
|
||||
duration: Duration(milliseconds: 500),
|
||||
timeout: timeout,
|
||||
);
|
||||
} else {
|
||||
final offset = direction == SwipeDirection.up ? swipeAmount : (swipeAmount * -1);
|
||||
|
||||
await world.appDriver.scroll(
|
||||
finder,
|
||||
dy: offset.toDouble(),
|
||||
duration: Duration(milliseconds: 500),
|
||||
timeout: timeout,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SwipeOnType extends When4WithWorld<SwipeDirection, int, String, String, FlutterWorld> with _SwipeHelper {
|
||||
@override
|
||||
Future<void> executeStep(
|
||||
SwipeDirection direction,
|
||||
int swipeAmount,
|
||||
String typeOf,
|
||||
String text,
|
||||
) async {
|
||||
final finder = this.world.appDriver.findByDescendant(this.world.appDriver.findBy(widgetTypeByName(typeOf), FindType.type), this.world.appDriver.findBy(text, FindType.text));
|
||||
await swipeOnFinder(finder, direction, swipeAmount);
|
||||
}
|
||||
|
||||
@override
|
||||
RegExp get pattern => RegExp(r'I swipe {swipe_direction} by {int} pixels on the widget of type {string} with text {string}');
|
||||
}
|
||||
|
||||
Type widgetTypeByName(String input1) {
|
||||
switch (input1) {
|
||||
case "MessageBubble":
|
||||
return MessageBubble;
|
||||
case "ProfileMgrView":
|
||||
return ProfileMgrView;
|
||||
case "ProfileRow":
|
||||
return ProfileRow;
|
||||
case "TorIcon":
|
||||
return TorIcon;
|
||||
case "button":
|
||||
return ElevatedButton;
|
||||
case "IconButton":
|
||||
return IconButton;
|
||||
case "ProfileRow":
|
||||
return ProfileRow;
|
||||
default:
|
||||
throw ("Unknown type $input1. add to integration_test/features/overrides.dart");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter_gherkin/flutter_gherkin.dart';
|
||||
import 'package:gherkin/gherkin.dart';
|
||||
|
||||
StepDefinitionGeneric TooltipTap() {
|
||||
return given1<String, FlutterWorld>(RegExp(r'I tap the button with tooltip {string}'), (input1, context) async {
|
||||
final finder = context.world.appDriver.findBy(input1, FindType.tooltip);
|
||||
await context.world.appDriver.tap(finder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
});
|
||||
}
|
||||
|
||||
StepDefinitionGeneric TorVersionPresent() {
|
||||
return given<FlutterWorld>(
|
||||
RegExp(r'I expect the Tor version to be present$'),
|
||||
(context) async {
|
||||
String versionString = "";
|
||||
final file = File('fetch-tor.sh');
|
||||
Stream<String> lines = file.openRead().transform(utf8.decoder).transform(LineSplitter());
|
||||
try {
|
||||
await for (var line in lines) {
|
||||
if (line.startsWith("wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-")) {
|
||||
versionString = line.substring(81, 88);
|
||||
break;
|
||||
}
|
||||
}
|
||||
print('File is now closed.');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
if (versionString == "") {
|
||||
context.expect(versionString, "#.#.#", reason: "error reading version string from fetch-tor.sh");
|
||||
return;
|
||||
}
|
||||
context.world.attach(versionString, "text/plain", "Then I expect the Tor version to be present");
|
||||
//context.reporter.message("test!!!", MessageLevel.info);
|
||||
print("looking for version string $versionString");
|
||||
final finder = context.world.appDriver.findBy(
|
||||
versionString,
|
||||
FindType.text,
|
||||
);
|
||||
final isP = await context.world.appDriver.isPresent(finder);
|
||||
context.expect(isP, true);
|
||||
},
|
||||
);
|
||||
}
|