Compare commits
1645 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c302b708a | |||
| 6a22919c50 | |||
| 9073ce5c7b | |||
| 9024c19169 | |||
| 60a0565ba8 | |||
| 383f7556e3 | |||
| 94795599e2 | |||
| 84fbb7c466 | |||
| c339f39b70 | |||
| 41a3609f06 | |||
| f5abd7acdf | |||
| 414368e251 | |||
| a3d1197aef | |||
| d91760eefc | |||
| ee20ced238 | |||
| 066892c11a | |||
| 69fd4f79db | |||
| f49e2768c1 | |||
| 70378b85d7 | |||
| 585401a98e | |||
| cf7ebfa03d | |||
| aec0a9951a | |||
| b113eec940 | |||
| a966812bfc | |||
| 3879a8ffdb | |||
| 5b949b0116 | |||
| 3c13619ce8 | |||
| 24bba98122 | |||
| a96e5e6ae6 | |||
| 4cfdfab31e | |||
| 77d3116431 | |||
| b943df1ce4 | |||
| 8bbb7d56e0 | |||
| 079a3d4fee | |||
| 176e0e7765 | |||
| c73e80f8d9 | |||
| 47cd1b568f | |||
| 058c523329 | |||
| 84515482a6 | |||
| 02629020df | |||
| 58d769b21f | |||
| 9dc67e0466 | |||
| 72d02104dc | |||
| 371a39049d | |||
| 47e4a6cf5a | |||
| 4a41e9f9a1 | |||
| 9fa1b58019 | |||
| c24473e176 | |||
| 1311ec498f | |||
| 251cec5dee | |||
| 1e15a8c1d3 | |||
| 9c5c58794b | |||
| 50063854d7 | |||
| 79d2041e46 | |||
| 479b27ce94 | |||
| a66857a7cc | |||
| 37815a3f39 | |||
| b55ba67b66 | |||
| 37a2d5fbca | |||
| d7b5c6bff3 | |||
| f11028529e | |||
| 93ec322bb9 | |||
| 71e0468d2c | |||
| 816e3442a0 | |||
| c37ed722dc | |||
| e08c2966c3 | |||
| 976f80ff7e | |||
| 34a4bda331 | |||
| a4077ccb4a | |||
| 21ada2a503 | |||
| 57a70c3085 | |||
| 16c8b88f0f | |||
| b806952430 | |||
| c0da0bd272 | |||
| 45239c2264 | |||
| 690236c4e5 | |||
| ebee3f72e6 | |||
| 37cec7d44f | |||
| 187fd63a75 | |||
| 362cdfc463 | |||
| 863b443317 | |||
| 341c474610 | |||
| cbb3c0911c | |||
| 890facc6f6 | |||
| 6fa8337058 | |||
| 3f1cb65e02 | |||
| 3551e7ec00 | |||
| 5ecf60a306 | |||
| 6659700a1c | |||
| 070174fee6 | |||
| 09003d85b1 | |||
| ea87108def | |||
| 7a696f9a62 | |||
| 8ba57a2733 | |||
| 9824cc2cbe | |||
| ad60cc72cb | |||
| 1950b80402 | |||
| 2acb47952b | |||
| 14cacaef86 | |||
| 958e815933 | |||
| 6b50be78c0 | |||
| 680223c4b6 | |||
| 1af914d5ef | |||
| a2fc710261 | |||
| 10922594b3 | |||
| abd80c5204 | |||
| ff589e3b91 | |||
| c80ccd70ec | |||
| d22d18da47 | |||
| 75b41c34ea | |||
| 11557e4815 | |||
| d698f74d0b | |||
| ecbea9fd95 | |||
| 13f7a64139 | |||
| 39cb1c638e | |||
| 489b58ad67 | |||
| f20fe33af9 | |||
| 6adddf4a0c | |||
| 16773c9b17 | |||
| 4b50365fa9 | |||
| 98766b9ebb | |||
| 45a739ce92 | |||
| c0d7145ada | |||
| f94c007af8 | |||
| df19cb5795 | |||
| e6ceb55092 | |||
| bfe2b5cba9 | |||
| 571004df50 | |||
| f32b59f0aa | |||
| e4019d8595 | |||
| 0b66a8701e | |||
| e62b8de1bc | |||
| d5cd790871 | |||
| 664c22d8f1 | |||
| 143a61e312 | |||
| baaad0e475 | |||
| 7086709082 | |||
| 7bd5ad8c0b | |||
| df19c91ae2 | |||
| e5872037e0 | |||
| b782fabbb6 | |||
| 485b466bd2 | |||
| 3beac6dfa9 | |||
| 98290a9fa3 | |||
| 13dd59f226 | |||
| d9c42a4135 | |||
| 644b93e5a3 | |||
| 3ff218f9c6 | |||
| f572eb5322 | |||
| d3eb480d31 | |||
| ac52b5b992 | |||
| 5c181e774f | |||
| 05d25718da | |||
| 66c50bef44 | |||
| 26bd59c378 | |||
| e90eae6080 | |||
| 86a7db7653 | |||
| 230de7e9dc | |||
| 4b8546a151 | |||
| ecd214b91b | |||
| 6b5de6e3e5 | |||
| 58b6e49aae | |||
| c480512600 | |||
| 3a5b6476aa | |||
| cb171092cf | |||
| 71979b34db | |||
| 73142cea39 | |||
| 2ab2c6f039 | |||
| 0bea15c0af | |||
| cbd78d78ba | |||
| ac0604a753 | |||
| 0e57335be1 | |||
| c4e64f6fa3 | |||
| bf9716f206 | |||
| 057ffdbaaf | |||
| 65dc0d3f34 | |||
| 173ee95e62 | |||
| 789339afa7 | |||
| 21b518da7a | |||
| 57b6b8dcf1 | |||
| 543a85316e | |||
| 2fedb3a0ee | |||
| ae450aed67 | |||
| 0abb4727fc | |||
| 4bc6eb96ff | |||
| e6a126d416 | |||
| fdf858f379 | |||
| 4151d123cd | |||
| c8a9759eba | |||
| c59b74627f | |||
| f2191d2996 | |||
| 7dfffbd50b | |||
| 329fc52077 | |||
| 8976111f61 | |||
| 7402959ac6 | |||
| 220d3877a2 | |||
| 380c33642c | |||
| 7acb2bef3d | |||
| 1a103106a5 | |||
| 6025e423e8 | |||
| 54656ea14e | |||
| fd00ed71b5 | |||
| d4fba5f3c7 | |||
| ce244f2e8f | |||
| 4ad466390f | |||
| c5c9b09f7b | |||
| 0638b31c1f | |||
| c3c713a75a | |||
| 9af1c72233 | |||
| 500a1e46ad | |||
| 4b446877af | |||
| 015548613a | |||
| 30b339a482 | |||
| 6dcb2e8d24 | |||
| 3e8e17526b | |||
| 30ecaf7aea | |||
| f761008509 | |||
| c3ab8dddd0 | |||
| 164f089d37 | |||
| a021b400bd | |||
| fac8f403be | |||
| d85ab37828 | |||
| 1e35403c87 | |||
| b99c2165fa | |||
| 303100bb6b | |||
| b71ba79b8a | |||
| 54cd84b842 | |||
| 1565ecdcea | |||
| 0a99b68d25 | |||
| f4fac5bd90 | |||
| f6760b90da | |||
| ad9b1f05b4 | |||
| 17581a7a5e | |||
| b41bf66133 | |||
| 8bb3d71472 | |||
| 295d4b9466 | |||
| 5e490376f4 | |||
| fa27531c00 | |||
| 2737e5613c | |||
| d84612ebf4 | |||
| 96165ad5a8 | |||
| 19caef057e | |||
| 29cafb11eb | |||
| 7e458bfde0 | |||
| 2a3cb80217 | |||
| 3d382ee15e | |||
| 6069dfc6f8 | |||
| dee19ed94a | |||
| 905b0681f5 | |||
| b6a4e1f145 | |||
| a0131bf39b | |||
| 7ed77a00df | |||
| 887c173d8f | |||
| 6362da7a50 | |||
| 1296365bed | |||
| 99ae7c5961 | |||
| 5c3ea712fe | |||
| bc5cb454bf | |||
| 8a7c2c1e20 | |||
| a81a675d59 | |||
| 1c66da7873 | |||
| afe3cd1098 | |||
| 4f3ee9ca1d | |||
| 7771aaa501 | |||
| 5ad38c7960 | |||
| 0fb1514da2 | |||
| f37efd7e15 | |||
| 1ae2464df1 | |||
| 0425b70d31 | |||
| 7b0d3f36dc | |||
| 14b917dc7e | |||
| 6184cc0307 | |||
| 870aa8e7b0 | |||
| d88016669b | |||
| a464b413d9 | |||
| d719edf104 | |||
| b36b00a11c | |||
| a99db2b16e | |||
| 2744dec43a | |||
| 6f2cce1494 | |||
| 689ee243aa | |||
| 537fc0ef5c | |||
| e647b31f29 | |||
| b59932cd88 | |||
| cfb4377de3 | |||
| e861c022da | |||
| 59006d3182 | |||
| 503faea3a9 | |||
| eb114de5c8 | |||
| 1bf9695cff | |||
| 241bf065e8 | |||
| e0f3b35805 | |||
| 5741dfc00b | |||
| ec430da772 | |||
| 5e6d9434de | |||
| b72d586748 | |||
| 757c0fd2ea | |||
| c4e4eaf110 | |||
| f83275e246 | |||
| d0340d39db | |||
| 227a279131 | |||
| 0465fdea62 | |||
| 13bd4a9c74 | |||
| f570f1f2c4 | |||
| 68ced18ea1 | |||
| b4a8f01980 | |||
| c3c743fbb8 | |||
| b14eddefc9 | |||
| 46638a1948 | |||
| 5cee85fcdc | |||
| f97d7e3dfd | |||
| 6da0ecf827 | |||
| 9803550bba | |||
| 15284da4c5 | |||
| 351c3219e4 | |||
| ab95dbbc77 | |||
| cc6cba45c6 | |||
| ce37660df2 | |||
| ca14ed9b2c | |||
| ba4cdea75d | |||
| 83c34dd4cc | |||
| b6db3802d3 | |||
| a9a19d3ae0 | |||
| 52fb873b1b | |||
| 9a0bb243cd | |||
| 78bbab37fb | |||
| 9af73b1409 | |||
| 9c5bb4aa17 | |||
| 49ba83dda8 | |||
| de3b0d4ca2 | |||
| b2efc42357 | |||
| a71faf674d | |||
| 34faa9003f | |||
| bc527a2bc1 | |||
| 0a3f96935a | |||
| 35232a3928 | |||
| 70d74e0bb1 | |||
| 36c91a95e2 | |||
| 4600e38a2a | |||
| 55abd88a03 | |||
| cd880b0879 | |||
| bbae6d876f | |||
| 48a0c5a5a9 | |||
| c261df41b0 | |||
| cc98eced27 | |||
| 452d5960e4 | |||
| c95b180728 | |||
| 3c380d35fd | |||
| 41935120e5 | |||
| 03d8f72c41 | |||
| ab9ecff4d4 | |||
| e351a0b235 | |||
| 4a08de370a | |||
| 6d657b449c | |||
| adef572abb | |||
| d6f2039bd1 | |||
| 1223c3c768 | |||
| 333fa22c96 | |||
| 76c04d8d6d | |||
| c3070f2913 | |||
| 234b3967ed | |||
| 89d420cda8 | |||
| ced4ece5b8 | |||
| 8c81e47737 | |||
| 5d15eef61d | |||
| 8f3e62245f | |||
| e4ab795c62 | |||
| e4d6f9240f | |||
| cfaf40e605 | |||
| bdcf2431e7 | |||
| 7241283be2 | |||
| dde2a8b63a | |||
| f7763a5b82 | |||
| c6f4a01001 | |||
| 95a6835988 | |||
| f9a8f447d2 | |||
| d20f588802 | |||
| f23476a4e9 | |||
| fd4864b3b1 | |||
| c5c0c432c4 | |||
| 69c40a6835 | |||
| 7ef7aa65e6 | |||
| 97c08f0d52 | |||
| 18e6c57e75 | |||
| ffc1463cda | |||
| 84e654efb2 | |||
| d983265e08 | |||
| e60b32202e | |||
| 95fbd7a31c | |||
| 00a91e32fc | |||
| fa32b7a883 | |||
| 63e6f955ed | |||
| 7dcb8a425a | |||
| f35ce068f9 | |||
| 881d231a93 | |||
| 293634c758 | |||
| 4134df3f35 | |||
| f78a019c70 | |||
| d561a1385c | |||
| 9b5387e221 | |||
| 25b1a814fe | |||
| b043b6e458 | |||
| 8a972d93e9 | |||
| 8fe66a14c5 | |||
| f82bd64c10 | |||
| 4bcab49539 | |||
| 0f4618ab11 | |||
| 475ca50fab | |||
| a64a02fa0c | |||
| f3669a5865 | |||
| 34dbd11db0 | |||
| 2e7279c72f | |||
| 6ad72f00af | |||
| b771a21518 | |||
| 04fb459acd | |||
| 690a68f0d0 | |||
| f34ae8d118 | |||
| da43ff1e95 | |||
| f053ebbd51 | |||
| 87606af29c | |||
| c811bdcffa | |||
| 0536628da3 | |||
| 1fa53cfcb8 | |||
| a9ea3854d2 | |||
| dc35261e00 | |||
| 716bc1f5e7 | |||
| db27204084 | |||
| 42aeceffe2 | |||
| 03845eabaf | |||
| 62af9dad50 | |||
| ee58d47926 | |||
| d74260b536 | |||
| 15d8a698c5 | |||
| 62cf3feeaa | |||
| 947ab7d48b | |||
| a82b9ee25f | |||
| 1e4d96b7c4 | |||
| 735a8e680c | |||
| d9e9fe1d6a | |||
| 4bcd1df4f8 | |||
| 9762899272 | |||
| ce1b73970c | |||
| 58282e589b | |||
| 75bd113545 | |||
| 7a6bd0e1f2 | |||
| f673c4eb83 | |||
| cbb04e8f0c | |||
| cd03da54d5 | |||
| 5f31f5966c | |||
| d8bbfe2678 | |||
| 7a2d408ca2 | |||
| 5e4dfcc65f | |||
| 7811e51b41 | |||
| 9703a868e5 | |||
| 1b7784b01f | |||
| a83abaca1d | |||
| 29b3f09d8a | |||
| d36b2a23f5 | |||
| 8f1722c718 | |||
| 5416c3b8aa | |||
| 89eeae36c4 | |||
| eec2685e67 | |||
| 318b59a6b2 | |||
| a2e0468cd9 | |||
| 689eacd618 | |||
| 8617a074ad | |||
| 046b8da880 | |||
| 34a36ddfea | |||
| 9330448198 | |||
| b3336b4d84 | |||
| 9553c94097 | |||
| c1845ae1c4 | |||
| b6cc3852b0 | |||
| eefc86f27e | |||
| 09404157aa | |||
| abfd9f8f41 | |||
| e04381fd75 | |||
| 30cc3ff9fc | |||
| 6f5f299035 | |||
| 02eed02cb8 | |||
| c1d29b5c39 | |||
| db4442939d | |||
| 6ece776382 | |||
| 0eda714755 | |||
| 831d099503 | |||
| fa23e4ca70 | |||
| 982f602178 | |||
| 713298109a | |||
| 8793981804 | |||
| 9bd4e9524c | |||
| 791dc2724f | |||
| ba3473c61a | |||
| 3ea194255d | |||
| ea081e981f | |||
| 2ce6ea9a2a | |||
| 295c9310e9 | |||
| 7447ed2eac | |||
| d5bf16b91a | |||
| 76665c1f0d | |||
| dd28523b05 | |||
| 16588c401e | |||
| dbf8a7ca87 | |||
| e92c76434e | |||
| 7adb581271 | |||
| 869476a41b | |||
| 8daf1bca20 | |||
| d044b3c931 | |||
| 0fcb19e1cc | |||
| 2a6977da75 | |||
| 26bd435bf6 | |||
| 91f8d6075c | |||
| 9ed9a330f4 | |||
| 8bbf6b790f | |||
| a277e9b307 | |||
| f8e6bcf290 | |||
| 3ba2b46bb0 | |||
| b50eab230d | |||
| 3f91824325 | |||
| 879e05148b | |||
| 78e36b85d4 | |||
| 544cc06f13 | |||
| 133b7ef3f1 | |||
| 08a407dc23 | |||
| 6c697fad8b | |||
| c904a7aa97 | |||
| ad131d7c65 | |||
| e12d2d1e98 | |||
| f01e044662 | |||
| 03d3ae7043 | |||
| 6b60a22879 | |||
| bbded8caa8 | |||
| 3a6352d2a3 | |||
| 8293d6bc4c | |||
| 56bdb28c2f | |||
| b081fb1e13 | |||
| 58c1f64dfe | |||
| 92b7147dcd | |||
| fa3a85c948 | |||
| 9da4513694 | |||
| de520036a9 | |||
| 97ca15a1c0 | |||
| 713a34a5e7 | |||
| d688280a30 | |||
| ebbf8fad4b | |||
| 5891c6fb2d | |||
| 7c96319fb6 | |||
| 0d652ccfd6 | |||
| d3718aa7ef | |||
| fcdcb9fd33 | |||
| a8f925def0 | |||
| 53cb125712 | |||
| 2a5793d96e | |||
| d460fa7ed4 | |||
| 5272b13c41 | |||
| a66ac42038 | |||
| 0014a2cba7 | |||
| 7a9c01e6e5 | |||
| 16402e43a5 | |||
| b1944da58d | |||
| 9081d3c826 | |||
| d4ae0ca4cb | |||
| 88f6ab915e | |||
| 939024faff | |||
| 4c6e7991df | |||
| 036d91c039 | |||
| 869c922532 | |||
| 217d15a853 | |||
| 931ffd0ba3 | |||
| fecac297fa | |||
| b0ea8d7df5 | |||
| f126df2120 | |||
| 42450024fc | |||
| 101db6e164 | |||
| 13bef94bf7 | |||
| 02792c5a6f | |||
| 303929090b | |||
| 7a24554b68 | |||
| 5b10aa6fa7 | |||
| e6eefac609 | |||
| 5f5a80dcbe | |||
| 7802448b24 | |||
| 16d231f718 | |||
| 62ca6cdd2f | |||
| 7d81ed1150 | |||
| 27812bb1ec | |||
| 6854f7eb2a | |||
| de86c5622d | |||
| 6bf1a4295f | |||
| 7de2f0f460 | |||
| 50149a3803 | |||
| d7ee9639fd | |||
| 7d5627b17b | |||
| e24c951d83 | |||
| e6a11c1ccf | |||
| 3f66981359 | |||
| 874f808d56 | |||
| 450dc2f368 | |||
| 7a69df42a7 | |||
| 1ce1e30d32 | |||
| 011f1d592e | |||
| 1d29b0166d | |||
| 6df1a68213 | |||
| b7ee6bfcb3 | |||
| c0cb2b5e12 | |||
| b38865bdc7 | |||
| 6f46331772 | |||
| 989bd662c6 | |||
| 359e593481 | |||
| b7e0fe22db | |||
| 61cfbd6852 | |||
| 02c0d3ed6e | |||
| e4d6c3aeb2 | |||
| 0c6761fcfd | |||
| 8f884fdd5c | |||
| 07cea1818e | |||
| 132bc15373 | |||
| d993748753 | |||
| 3372565a39 | |||
| 134ac2b2fd | |||
| 0e0e91b4fe | |||
| 25b50bdb8f | |||
| 1988085171 | |||
| f892e9baff | |||
| 4828d84caf | |||
| aeae6ac292 | |||
| 0544c1f249 | |||
| 5027159ed8 | |||
| ce778be895 | |||
| 9e349d2b30 | |||
| 72f19758db | |||
| 55bce1fa12 | |||
| 5e1ebaa5d4 | |||
| 742c348998 | |||
| 9d46b52786 | |||
| ef374952ab | |||
| f8ef4d5985 | |||
| 85929809f0 | |||
| 068540120e | |||
| 471c4fc200 | |||
| 398c67362d | |||
| 4ceeda5f02 | |||
| 2bf6b993fe | |||
| 68363c5b82 | |||
| 9f47a41017 | |||
| ba70101efd | |||
| 3aa54c9982 | |||
| 825ca0d737 | |||
| 6754fef164 | |||
| 4c079a8c25 | |||
| 6e09d101b5 | |||
| 39aa583297 | |||
| b08db7a8c5 | |||
| 865bf0d056 | |||
| d52c520c02 | |||
| 1eabf11cdb | |||
| cfb16d3f17 | |||
| d5707638a6 | |||
| 5cda5db7f7 | |||
| 5c5d55d265 | |||
| 4dd3b92eda | |||
| 112579079f | |||
| 9897ba4b28 | |||
| c64dfff4c7 | |||
| 915b3f0cd3 | |||
| c295d11fc4 | |||
| bc47c5436d | |||
| aaeba4efe1 | |||
| 3c0eb58381 | |||
| c4f22449f9 | |||
| bca346ec2f | |||
| e0bd60f87c | |||
| aeedab1531 | |||
| c959f41c68 | |||
| 9ba755da16 | |||
| 34026c5538 | |||
| ea64425456 | |||
| eb34a20195 | |||
| 445513cc32 | |||
| e431518a9d | |||
| 61df88e094 | |||
| 891c130e12 | |||
| b4ced5278e | |||
| 10364e9342 | |||
| 74dc222a54 | |||
| 2e4ac7ede1 | |||
| 184c1b67cc | |||
| f702338129 | |||
| 4b4b263423 | |||
| 83c16a46de | |||
| 6383896a79 | |||
| 5fa1560a10 | |||
| 9bd6ad36cc | |||
| 83cc7d5181 | |||
| 44150673e9 | |||
| 5092d723a8 | |||
| 218964cbda | |||
| ccc9752485 | |||
| 619038f27d | |||
| 9f197b12ed | |||
| 690608cdf3 | |||
| 4035932340 | |||
| fc9d94701c | |||
| 6d54ae5f3d | |||
| c53abe0941 | |||
| 276e253fdf | |||
| f160e960be | |||
| 41b57b9207 | |||
| 56eae8c7bf | |||
| 46c8b3b690 | |||
| 58b11f3c47 | |||
| 40b4b316b3 | |||
| 7a31f69aea | |||
| 648c99e81d | |||
| 56b482a26f | |||
| c6df4af53a | |||
| 32fe927bfc | |||
| 5740b768d0 | |||
| d8e74c730a | |||
| 58846bbf42 | |||
| 78d30fc479 | |||
| 86afa988a0 | |||
| 6104ef62df | |||
| 3f89acf9bd | |||
| 591d499462 | |||
| c31a7152bc | |||
| 343cc3ca67 | |||
| 23e18cee22 | |||
| 5b2c458bcf | |||
| c10c64a6a6 | |||
| 957221e118 | |||
| e18e4454e4 | |||
| e1067e30de | |||
| 09b0f15294 | |||
| b1d6ff4bbd | |||
| a49e9dd96d | |||
| 5e428e2c4d | |||
| 0f6ff3c101 | |||
| 1ade8b502f | |||
| cc25f0685c | |||
| e91ed88785 | |||
| 39bc6d5eb3 | |||
| b7f472b0cd | |||
| 942f4a45bf | |||
| 767896b14c | |||
| 8c35628863 | |||
| d555370076 | |||
| bbbe76697d | |||
| fc1d60e65b | |||
| 9dc856202a | |||
| 6bc41776b1 | |||
| 940cee0f30 | |||
| cdb6c16473 | |||
| c4842ae7c5 | |||
| dc32e51ac2 | |||
| 43caaf7efc | |||
| dcd0d433b0 | |||
| 763e891dfd | |||
| c04f761f5a | |||
| b147882e4f | |||
| c9f5f91aad | |||
| 0a3de42729 | |||
| 64fc0209f4 | |||
| 418ad51e77 | |||
| 16faf41a84 | |||
| d5cf8d36b3 | |||
| 755fafb0b6 | |||
| 8fc9893ecd | |||
| 9071fd0024 | |||
| 9a3233bb28 | |||
| e77bc9170a | |||
| 23d6a71a3b | |||
| 67c3f41dff | |||
| e22fa499c2 | |||
| fdef13ae92 | |||
| c0a6f2316c | |||
| 7d6a87c825 | |||
| 6c863fe99c | |||
| 5cf8242ea0 | |||
| a804e8a27c | |||
| 3b598e2f07 | |||
| 03c5a254e8 | |||
| bdb34e16c6 | |||
| e7c018283a | |||
| ebd8d85a3d | |||
| 3f8a9e1be2 | |||
| 0d5961baf9 | |||
| 872ee805d1 | |||
| b19bcd88b9 | |||
| 5c9d65386b | |||
| a86a0938ce | |||
| a886e5f9a0 | |||
| 83c1bd61cb | |||
| 4ce1789110 | |||
| f484fdbbac | |||
| 57ac7cb328 | |||
| 47cdc50a81 | |||
| 555ddb5b20 | |||
| 8e8ba23da7 | |||
| 54a1b97167 | |||
| 7530d44d28 | |||
| 252aa3714e | |||
| ce09e9a217 | |||
| 8797236b5a | |||
| 6097e6c305 | |||
| 879fca0e11 | |||
| c359ddf3c8 | |||
| 8ad77ac7aa | |||
| bd3b779282 | |||
| 42b805eb91 | |||
| 107f2cd3b1 | |||
| c713ccf76c | |||
| dd9c65012b | |||
| 31e872a34e | |||
| 81579dc9bf | |||
| 3c6c03cd75 | |||
| ba41df19bb | |||
| 0cc7178cdc | |||
| 239e4a7e66 | |||
| 0b031d35e3 | |||
| 32e81049f5 | |||
| 6a65a1c149 | |||
| 36ea2a7f5d | |||
| cc40c9d09f | |||
| fb9e731e00 | |||
| 264607e0f3 | |||
| cdfc42cc38 | |||
| 19cfae1da5 | |||
| 577b11a349 | |||
| 671dfceac3 | |||
| 5626fb74ae | |||
| 88d6c91517 | |||
| aa76cefb1c | |||
| a4fde60c1c | |||
| 8e86612fc2 | |||
| f6ded23383 | |||
| 2ab689c59b | |||
| 155f6a88f8 | |||
| 3d84fc9c98 | |||
| 976e146248 | |||
| d9b0723194 | |||
| 0630a6910a | |||
| bc930345b9 | |||
| d7d7963101 | |||
| fa2551dfcf | |||
| b260a47b49 | |||
| 47e55fc621 | |||
| 700fe5e463 | |||
| ca3d239ce2 | |||
| 12385b9c5a | |||
| 65b26adb3d | |||
| 31b8927291 | |||
| 94ffbb3e8e | |||
| 1b7616b4db | |||
| 71850e1e35 | |||
| 380fb60791 | |||
| 2cf9fa0524 | |||
| c8c4fdc65e | |||
| 9fb16be74f | |||
| 2d4d4aab66 | |||
| f18070b78c | |||
| adf1d8a43a | |||
| f360934ddd | |||
| b986c4d54c | |||
| 4545e70384 | |||
| 3c5cbc3114 | |||
| 879b0ad95d | |||
| 9982810edb | |||
| 699b788187 | |||
| bd61044643 | |||
| 6804d58323 | |||
| bfe57b4b8f | |||
| e3fe852a34 | |||
| 673fe2625c | |||
| e798feb1d7 | |||
| fa937f9f43 | |||
| ec5f3fc333 | |||
| d85c150a8c | |||
| a5faf0e098 | |||
| 1234c63836 | |||
| 91920319c7 | |||
| 950d9d5a4c | |||
| 467dae8132 | |||
| d1ef9d5dcf | |||
| a60419a442 | |||
| f97a034c34 | |||
| 4d0fbe2343 | |||
| 1d5e108cd4 | |||
| 716afc98ac | |||
| 459607adae | |||
| 784b705265 | |||
| 326b95bd10 | |||
| 466acaf504 | |||
| 838165c3e6 | |||
| e1d7ad7d03 | |||
| 3776e86b83 | |||
| ea60858a07 | |||
| d4488c72fb | |||
| 9146f2fb30 | |||
| 92993f967e | |||
| 2c2735af6d | |||
| 80c0e19692 | |||
| c9e2162afc | |||
| 9a52f4e3ff | |||
| c0235d4cc2 | |||
| f1704fbb57 | |||
| 38d5d3ad1b | |||
| ec96b4e3aa | |||
| aa33fd44b8 | |||
| 98865d61dd | |||
| 0036b8e2d6 | |||
| c021d26103 | |||
| 8bca5b4901 | |||
| d4db6c8912 | |||
| c93b4909f4 | |||
| 67d3c8e777 | |||
| 8d44222097 | |||
| 9cb2024334 | |||
| e71bb33b23 | |||
| 4ada7c9be9 | |||
| 56a2d8891f | |||
| 4585e90a00 | |||
| 86d2ddc168 | |||
| 9d1514308a | |||
| 7abff55981 | |||
| f9d7eba7d4 | |||
| 9ce021afa2 | |||
| 6fc9055221 | |||
| a3438d3345 | |||
| d2cbf11264 | |||
| c584156c86 | |||
| 78e04f3ad8 | |||
| 6302725678 | |||
| 431e65808d | |||
| 653914f47e | |||
| 96823e944d | |||
| ee19520e1b | |||
| 6f16b3fee7 | |||
| 89ee7e8e19 | |||
| 3c7996aa99 | |||
| 3a314c565c | |||
| 8c9b668cd7 | |||
| 7666462de2 | |||
| 54cf11a78b | |||
| 16b78f0843 | |||
| 5e97a6b192 | |||
| 595cced5b7 | |||
| d17f12dd76 | |||
| 8b5498cfbd | |||
| e4b755ced8 | |||
| 5b7eb9c332 | |||
| cd8e07c102 | |||
| a36f31c2d0 | |||
| ac0812a6dd | |||
| 69c864f984 | |||
| 3c9a7fd329 | |||
| f81dc11f61 | |||
| ce9a8f62d4 | |||
| 7e00d50078 | |||
| 4af3f5038f | |||
| 7bb1c58452 | |||
| 8e7383be05 | |||
| bc5d27ed90 | |||
| ae884d79a1 | |||
| cf89c988cf | |||
| c54313c32e | |||
| 3e001ddf1b | |||
| c41795e7f0 | |||
| 52120afdbd | |||
| 73d98da32b | |||
| 99f936ff97 | |||
| 15afaeabe3 | |||
| debf964b5f | |||
| 393730cea9 | |||
| 2194fbd535 | |||
| cf59249d3d | |||
| 2c554a3a20 | |||
| 7b9554a42c | |||
| dd527ce33c | |||
| ddcc06c6b7 | |||
| a827033f25 | |||
| 01841a4aa8 | |||
| bb52e7159c | |||
| 3988b46a60 | |||
| caa5e233df | |||
| c7609f9a2a | |||
| 750fd4efe1 | |||
| e361795184 | |||
| 64fff2adb2 | |||
| 846e398577 | |||
| c1e9ee7a66 | |||
| 8dc9e09f31 | |||
| d1930d4936 | |||
| 14539eb036 | |||
| 45f1853c44 | |||
| 6eaebd112b | |||
| f0503faeff | |||
| 64052d9dd2 | |||
| db4634a0dd | |||
| c725a2fabb | |||
| 1ddececa16 | |||
| 4e2e6cd83e | |||
| efcfe2dafc | |||
| b8ddb9e673 | |||
| a1f19e9d8a | |||
| 5464edf639 | |||
| 179c3790e6 | |||
| cfae9753a3 | |||
| 61a4a3b322 | |||
| c16bf65a80 | |||
| 16ea1912b4 | |||
| 54012cb33a | |||
| 459c5c0a55 | |||
| 4216b56443 | |||
| d7b79314d9 | |||
| a340b13f65 | |||
| 72f6b15dba | |||
| 64dbb77e63 | |||
| af10b0e4f6 | |||
| 6f15c16a42 | |||
| 86158027d7 | |||
| 50369890f7 | |||
| b8dea25aef | |||
| 64e9324aa0 | |||
| 20f8c69b07 | |||
| dd1a15c249 | |||
| 8b24498fa7 | |||
| 3673fa4908 | |||
| 960c1df5e7 | |||
| 8c3c7c18ad | |||
| b96a5af133 | |||
| d0d4008100 | |||
| 17a6fcafa1 | |||
| fb75440769 | |||
| fe39b5e4e2 | |||
| 62b142cdeb | |||
| ffce7213b4 | |||
| 4205934806 | |||
| 7aab86643a | |||
| 1bb0c55d88 | |||
| d22ac9ee00 | |||
| 80a7db2511 | |||
| e0fb102572 | |||
| 8d1a16dcd6 | |||
| 0b4bbd5db2 | |||
| 78b714e019 | |||
| 5022d81d9a | |||
| deacf28d77 | |||
| 5e8d324860 | |||
| 3554f82ea3 | |||
| 888a40a5c4 | |||
| 363953a0a4 | |||
| e599d9b14e | |||
| a33be1fad3 | |||
| b6528e843e | |||
| 10c31e6591 | |||
| 8fba64cb8f | |||
| c2fd08ca80 | |||
| 940bf0603e | |||
| 4d8a3dafe0 | |||
| d237bb0136 | |||
| d42dfd3edd | |||
| ab4f17d55f | |||
| 07968febe8 | |||
| 67ff0892d5 | |||
| f1ee168657 | |||
| 5fef60c2b0 | |||
| 4afffc7dd3 | |||
| 7e6346a694 | |||
| abf22eff44 | |||
| 3fa3b93c85 | |||
| 549ef9dabc | |||
| 59c75663b1 | |||
| 820a5bc363 | |||
| 1b9cf631be | |||
| b4f2208bae | |||
| f4bcfca323 | |||
| f93a9a0f22 | |||
| e5652197eb | |||
| 4a102d44cb | |||
| c837840e04 | |||
| b280ff7495 | |||
| f6d8dcf6fd | |||
| fa0661f58a | |||
| e2fe137b05 | |||
| b434e955ac | |||
| d74b302edb | |||
| 0200430346 | |||
| d70ebc2398 | |||
| 2b606a2dec | |||
| 9c7f2250b9 | |||
| c2ee621f64 | |||
| b2cdb46c84 | |||
| 6d150aa5cb | |||
| 62ece66f36 | |||
| 628cd3896c | |||
| f10418face | |||
| ca9a629804 | |||
| bb30535afb | |||
| 624f863da4 | |||
| b55a9f253e | |||
| 5b9ef5b6b6 | |||
| e7c8ecbd31 | |||
| 592dfec8db | |||
| 23ebccc041 | |||
| 036bd51298 | |||
| 6d9a66cc41 | |||
| 1923b84a01 | |||
| 9924e293c9 | |||
| 490d3549e2 | |||
| 45d2a5d0b6 | |||
| 4d3929948c | |||
| 56ea09431f | |||
| a53a5f4685 | |||
| 52f3ff5ff6 | |||
| 7150783848 | |||
| c03d3520d6 | |||
| d2e19c5129 | |||
| a829165f2d | |||
| f2707d053d | |||
| 2a4ccf69b2 | |||
| 818356dfed | |||
| 49d6743cbb | |||
| 9ed80d46b6 | |||
| c2f5a6390e | |||
| f2a7824168 | |||
| 3439861f74 | |||
| 06ee096746 | |||
| 6230a7553d | |||
| e17b07bb12 | |||
| ea2d4e9206 | |||
| 2fffc86a5a | |||
| 1bb0af72ee | |||
| 23a58ac064 | |||
| af9d16852e | |||
| ee47c1ea10 | |||
| 2b318152fa | |||
| 10a363248e | |||
| 11d4bde18a | |||
| b88b992cb6 | |||
| 6e3e1b56fb | |||
| 7dfda2598d | |||
| 853862c475 | |||
| 5627bb6bed | |||
| b646e69b6b | |||
| 632aeed00b | |||
| 3f5b4bad62 | |||
| 7bba4ed820 | |||
| e22ff1bbfe | |||
| ab66567db6 | |||
| a763e1729c | |||
| 6aac250990 | |||
| a749b97707 | |||
| f966b23f3a | |||
| 763025d19b | |||
| 0bf2ae6075 | |||
| 71f947484e | |||
| 5160164111 | |||
| 7501e029ab | |||
| a678555d8d | |||
| 7ce2991b0f | |||
| befa396e82 | |||
| fb69fc5af2 | |||
| b540b5813e | |||
| feb74d90f6 | |||
| a0de2577e8 | |||
| dbc5112ada | |||
| 9f8335810c | |||
| c54e2388ce | |||
| a8a7019411 | |||
| 098da3c3dd | |||
| 71b5645801 | |||
| f5d9fbe91c | |||
| 420e15c179 | |||
| 74619f6f8d | |||
| 1355a4a28d | |||
| 97c34b889a | |||
| 0b0c54d874 | |||
| 1005be006f | |||
| 8db113a19b | |||
| 075df8a26d | |||
| 38cf3f40e1 | |||
| 4a0abbbee7 | |||
| 15f1201a76 | |||
| b152723ed2 | |||
| 84a2832a65 | |||
| 8037494f7a | |||
| 97c1ace020 | |||
| 64457b0235 | |||
| 67ef831681 | |||
| 1fd6aae3d9 | |||
| 61810cc977 | |||
| 59401e18ed | |||
| 30eff93fa1 | |||
| 7c5bae3b53 | |||
| ee16e4236e | |||
| 30e9cf9dc8 | |||
| ac5d0bf8a3 | |||
| 923eb05e59 | |||
| 8f59e51445 | |||
| 766733617e | |||
| d77744c562 | |||
| 0d6db1305e | |||
| 61c2e59f41 | |||
| 47dd7adf4b | |||
| 016736c455 | |||
| 6d3924ba43 | |||
| 428f963243 | |||
| dd871b64ea | |||
| 38863f618a | |||
| 8023285b9d | |||
| 1aa7175006 | |||
| 1222c30738 | |||
| 0c8e62add9 | |||
| eb1d06b4a6 | |||
| d58c3292d7 | |||
| 4320d26a3d | |||
| 3ca4e33d94 | |||
| 19e726a630 | |||
| 96dddef271 | |||
| 34a228f85e | |||
| 213d996168 | |||
| 5a159ce01f | |||
| fed9c64113 | |||
| 2d835581a5 | |||
| c8f1ebdf4c | |||
| 98e3530acd | |||
| 1a5b216dd5 | |||
| ae98d5e3bd | |||
| 750825b3c3 | |||
| 8c255256c9 | |||
| 19626361ec | |||
| df4bd1fa4a | |||
| 62bf5abd8d | |||
| cd9ec9f346 | |||
| cf7d5b3481 | |||
| 12f9ac3aa4 | |||
| 4519cdb49c | |||
| d20b6f355c | |||
| 70e64003f9 | |||
| 0a4644e743 | |||
| c428d23d8b | |||
| d6b189badc | |||
| 6e899391c0 | |||
| e0acbcc32d | |||
| 95fb9ea117 | |||
| e80b7cf0a2 | |||
| 5e70c06075 | |||
| 1413b74f76 | |||
| bf0548e802 | |||
| b7e1863526 | |||
| f189188563 | |||
| 2f52664820 | |||
| 5f6fa73be9 | |||
| b7ec913cb9 | |||
| ebef4b079c | |||
| a81e5c4e6b | |||
| b0733dcd51 | |||
| e9bd35619d | |||
| 6528b34152 | |||
| b60c02e0c7 | |||
| a0792d166b | |||
| fcf36c4bc0 | |||
| e5b617cd16 | |||
| 0acefb4521 | |||
| 111c8367a9 | |||
| ead8f209b6 | |||
| 96333b616b | |||
| 5698e0deda | |||
| df2ddebf6c | |||
| 71ab7528e7 | |||
| b4e459d831 | |||
| a57d3fdf3f | |||
| 2c207873be | |||
| fc8385113f | |||
| 95d7d26f11 | |||
| 43a13964bd | |||
| d2053d2db7 | |||
| 8ba2bcaa53 | |||
| f7abdbe97f | |||
| 91af3e60ba | |||
| 8fe196cd7a | |||
| 66d7241c03 | |||
| 89d7c0b0d0 | |||
| d2ec62d681 | |||
| b6d38fe8f1 | |||
| 1edc256148 | |||
| 24ac385898 | |||
| f062e58f7b | |||
| 96aec401b9 | |||
| 7ff0b7aa3c | |||
| e5ab5241d5 | |||
| 0f4f87067e | |||
| 3f32f816b0 | |||
| 73de2dfda7 | |||
| d6fd6cb5a3 | |||
| 39fbbe896f | |||
| 29c70acf4e | |||
| 5cd2568776 | |||
| 60a6535a12 | |||
| f48b389449 | |||
| 316dd210a0 | |||
| a60712c09d | |||
| 482cd564ff | |||
| ac1171d43b | |||
| ed8953c430 | |||
| 9a8aecaf3f | |||
| 423719e7bc | |||
| 7f2b6a874e | |||
| cfe5ea3f9b | |||
| 07aa058a46 | |||
| 6cadf93c43 | |||
| 60eb1332d2 | |||
| a9ee7e93fd | |||
| 2782216e52 | |||
| d22537c5f2 | |||
| 57aa6c19e1 | |||
| 761553d392 | |||
| 29350ab7b0 | |||
| 528ccc1e9d | |||
| 20d26ad7ca | |||
| 5d23c5c902 | |||
| 145794bf04 | |||
| d00f2aa8d0 | |||
| 3a20375567 | |||
| 7be93a8a44 | |||
| b5e4c4e92a | |||
| 20285796bd | |||
| 7826ff94e3 | |||
| f1dccbb64d | |||
| 528e301ce4 | |||
| af016a9c79 | |||
| cbd5738543 | |||
| 2dd0899a3d | |||
| e486a4baef | |||
| 5fc11baf9e | |||
| 157777cac1 | |||
| 99d0ee6725 | |||
| b5c1051506 | |||
| bba3334df5 | |||
| 74488feec2 | |||
| 54953abc67 | |||
| 117bbdbcdf | |||
| b96b99c1c4 | |||
| 6e856a7648 | |||
| 0659edb762 | |||
| dcb870c432 | |||
| 772bafbe43 | |||
| a9be6aff44 | |||
| dcd7ec7383 | |||
| c69a4dda00 | |||
| a911926119 | |||
| 6f30aec4f2 | |||
| 5a005fb809 | |||
| 776a4c5dce | |||
| c53c316303 | |||
| 622aa844e4 | |||
| de2cf6026e | |||
| a8e02b9ced | |||
| 297308ad76 | |||
| ea0c3dbe5a | |||
| b8d229e58e | |||
| c4f5110148 | |||
| 7fdd7e89bd | |||
| 2378346537 | |||
| 72fc5fc3b1 | |||
| c063c99ba6 | |||
| 90341f0a6e | |||
| cdb9df5aba | |||
| 1f6d9d6422 | |||
| ffbda7e521 | |||
| 3b5ef29047 | |||
| 14cf6ceb84 | |||
| 5fb940ff2a | |||
| f446e18289 | |||
| 84f26b32d6 | |||
| f7690245aa | |||
| f44e32fd6a | |||
| 8bac34238e | |||
| 6d2f6ce2f9 | |||
| 3a465cc56b | |||
| 617369dbc0 | |||
| c0fed1498e | |||
| 5bdd3ce47a | |||
| 6b3f41d675 | |||
| 23b696c9cf | |||
| 079400f89e | |||
| e12d467627 | |||
| 162ca3e21e | |||
| dddd0e7b71 | |||
| 95d68e09da | |||
| aaf0cf53d8 | |||
| 9c8f759732 | |||
| a45c685893 | |||
| 87bdebb21c | |||
| 4f754ae309 | |||
| 4b004f70ec | |||
| d468d4c21b | |||
| a4df433d80 | |||
| 10eec025d2 | |||
| d497ed4195 | |||
| e63137d293 | |||
| c744743913 | |||
| 42493c8eb6 | |||
| 391839028f | |||
| d9ecfeadc0 | |||
| d866646f66 | |||
| 6295041341 | |||
| 8c7556427a | |||
| 82c91db78c | |||
| 2d969f4fff | |||
| e84d46dae7 | |||
| b6828b54ca | |||
| f9bd1bac36 | |||
| 22e2bfacae | |||
| c446d4bb54 | |||
| 23c7e5dc3f | |||
| 661f1e624c | |||
| 81ff5ef899 | |||
| e79364cb03 | |||
| d750e2fe7a | |||
| 5e1025453a | |||
| 280da481ee | |||
| 9da5f47623 | |||
| 45f1f419e1 | |||
| 92f2ac67d5 | |||
| d28a62d70b | |||
| f9336f2a28 | |||
| 940e67b1ca | |||
| 073e138ab2 | |||
| 5aec4b4571 | |||
| f9cd3decb1 | |||
| 627c47b155 | |||
| 57135ea2c6 | |||
| 609e9fcdb0 | |||
| 5b0e71b680 | |||
| 9c2d478797 | |||
| c55fa13038 | |||
| 27b9565d2f | |||
| 4fe6d79fff | |||
| e636e38ba1 | |||
| ebc6665224 | |||
| 7001cedbc7 | |||
| b14209d5cf | |||
| 5150564fe2 | |||
| b7eaa9e353 | |||
| c00943591d | |||
| 1f9320200a | |||
| 6a6b80cce2 | |||
| 05296e3d9b | |||
| 7e68050e0a | |||
| ab928be1b3 | |||
| 65d26d753d | |||
| bf37c09ba0 | |||
| 89199b81ab | |||
| 0dd17673f5 | |||
| c17d6c2334 | |||
| 5285dd1665 | |||
| 046ce30e08 | |||
| 1601fa5608 | |||
| 5f7099184d | |||
| 8425bb4f59 | |||
| e44006f531 | |||
| 3423e24de6 | |||
| 5ac363232f | |||
| 9cc020a2c7 | |||
| d2240f07d8 | |||
| 4968db750b | |||
| 6134244244 | |||
| 4559ca9f2b | |||
| 9a38920cb8 | |||
| 2b771931e6 | |||
| d72e003f8c | |||
| 097988e046 | |||
| 4d15bc7ea0 | |||
| 26f49e2877 | |||
| 10aba86e70 | |||
| 9e3d100599 | |||
| a7193e321c | |||
| fa15469696 | |||
| 58b9cdf28f | |||
| 8e05fe3b0c | |||
| af063b2e9e | |||
| 5cc85cc860 | |||
| eafa1eabee | |||
| 34a1838668 | |||
| df83c94180 | |||
| e102b60923 | |||
| 02900eaa6d | |||
| 5ed4c51582 | |||
| 81e928f94e | |||
| 985b569d29 | |||
| d2d000ef16 | |||
| 520b3a14bc | |||
| 157d194cc5 | |||
| 2785609481 | |||
| 6e5e60173b | |||
| f37e938f17 | |||
| da645acd1c | |||
| 17205b2baf | |||
| b5ba4d3570 | |||
| 17b24d3c24 | |||
| 044454dca2 | |||
| 88bff9ab6c | |||
| 203fde60d6 | |||
| 82956c4149 | |||
| 1f41b9e481 | |||
| 945921fa9a | |||
| 7d5786ea93 | |||
| 6be1413d7d | |||
| fd07ab10ee | |||
| 6232656ad4 | |||
| 8493c7ffe5 | |||
| 15700b85cb | |||
| 3dfd1c98ba | |||
| 9a249b0dec | |||
| b74a431ac9 | |||
| 880ce18fd0 | |||
| 6279149cb8 | |||
| f5c5a34798 | |||
| e9a616c68d | |||
| f5ee7160cb | |||
| cea671aab5 | |||
| da84cde6da | |||
| e9fbce4e28 | |||
| 913605a065 | |||
| 4bf49df6fa | |||
| 91a9d6c68f | |||
| a477c3c4d9 | |||
| 0cdd56e0ac | |||
| abefb894cc | |||
| 97d482c1ad | |||
| d3e9303d6d | |||
| df7bb13752 | |||
| d28f6f5922 | |||
| c90ad7c1e2 | |||
| 7fbdcb8a88 | |||
| d46daed49a | |||
| f18a03ee6d | |||
| 1d052e7c1b | |||
| 2611165f21 | |||
| f059aa7407 | |||
| ac27df1f0e | |||
| 76b28593ea | |||
| 0940c88c20 | |||
| c3408040fc | |||
| d2ffc11749 | |||
| 4d640ec467 | |||
| c409d49f14 | |||
| 2c0dbf1062 | |||
| 25f0208e61 | |||
| d063cfe36a | |||
| 5c089e1d77 | |||
| 867006d29c | |||
| 6a974c48ef | |||
| c314918c6b | |||
| e2e2a076c7 | |||
| 8ee12b9f26 | |||
| 7377293f81 | |||
| 29ae49b5f1 | |||
| 195d967b3f | |||
| eac74bf9c1 | |||
| 9f2dbf7b6c | |||
| 9e836ba586 | |||
| cc6dc1b3a2 | |||
| f49da2c9bf | |||
| 96c1077238 | |||
| 8d72b27e1d | |||
| 0ea0d139dd | |||
| b81ff4d672 | |||
| f380ac5e43 | |||
| 962d42292d | |||
| 15df15556d | |||
| 6b29841cc8 | |||
| 4f4c1a9bb8 | |||
| 5f7630b906 | |||
| 8a831889f9 | |||
| bce133ac28 | |||
| f5215d715a | |||
| fde0f3bba1 | |||
| e7b18bd3a2 | |||
| e5e86e639a | |||
| f44b44a354 | |||
| b3399b5242 | |||
| 7d4ebd9d3b | |||
| 3bb2131375 | |||
| d7314ec2a4 | |||
| cc6c724ee8 | |||
| d3b0559b72 | |||
| 1e24caec31 | |||
| 65cdc143da | |||
| 5d612f020c | |||
| ccef2cc178 | |||
| 9337160583 | |||
| bf9d570c3d | |||
| 306b0096be | |||
| 45583ea469 | |||
| 15c6c372ba | |||
| 770a89507a | |||
| ddc9aa7506 | |||
| a7d9fd19d9 | |||
| 091f7c49ab | |||
| b443f59078 | |||
| 27bcf92e9b | |||
| 31100c3d82 | |||
| 119da2e76e | |||
| 588a6cf74f | |||
| eb6394eb6a | |||
| 76de183ec2 | |||
| ba31ceb3e7 | |||
| e94e0f8a6b | |||
| f8283acfae | |||
| f8cb26ca74 | |||
| 190b9da6c7 | |||
| f84b46148c | |||
| 12db8b5ee1 | |||
| 05b5078aa9 | |||
| 85b7ee85f3 | |||
| 326b728d4b | |||
| 2e45e131b1 | |||
| 1aa95c057b | |||
| 6de7a849b3 | |||
| 268091b10e | |||
| 3920c85ab7 | |||
| 524565f0bb | |||
| 69c1c856d9 | |||
| dd62d92ffb | |||
| f7e89d75a4 | |||
| 023f31eadd | |||
| da8df5beac | |||
| f3a8825cb9 | |||
| 835fd47482 | |||
| efbd5cab85 | |||
| a6b7d0bcc5 | |||
| e06126d889 | |||
| 4bf8e2c488 | |||
| 1c55ad21a3 | |||
| 3a601e1e65 | |||
| c953003c2f | |||
| 18de51a531 | |||
| ab6d3b5e8d | |||
| 151980c6de | |||
| 375527b765 | |||
| 2978e567d4 | |||
| 8ad50ab61c | |||
| 2145ded2f2 |
@@ -6,4 +6,15 @@ ij_kotlin_allow_trailing_comma_on_call_site = false
|
|||||||
ij_kotlin_allow_trailing_comma = false
|
ij_kotlin_allow_trailing_comma = false
|
||||||
ktlint_code_style = intellij_idea
|
ktlint_code_style = intellij_idea
|
||||||
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
||||||
ktlint_standard_class-naming = disabled
|
ktlint_standard_class-naming = disabled
|
||||||
|
|
||||||
|
# below rules disabled during ktlint version migration because they were preexisting but should be corrected and re-enabled ASAP
|
||||||
|
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||||
|
ktlint_standard_property-naming = disabled
|
||||||
|
ktlint_standard_enum-wrapping = disabled
|
||||||
|
ktlint_standard_multiline-if-else = disabled
|
||||||
|
ktlint_standard_backing-property-naming = disabled
|
||||||
|
ktlint_standard_statement-wrapping = disabled
|
||||||
|
internal:ktlint-suppression = disabled
|
||||||
|
ktlint_standard_unnecessary-parentheses-before-trailing-lambda = disabled
|
||||||
|
ktlint_standard_value-parameter-comment = disabled
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
### First time contributor checklist
|
### First time contributor checklist
|
||||||
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
|
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
|
||||||
- [ ] I have read [how to contribute](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md) to this project
|
- [ ] I have read [how to contribute](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md) to this project
|
||||||
- [ ] I have signed the [Contributor License Agreement](https://whispersystems.org/cla/)
|
- [ ] I have signed the [Contributor License Agreement](https://signal.org/cla/)
|
||||||
|
|
||||||
### Contributor checklist
|
### Contributor checklist
|
||||||
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
|
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
@@ -30,7 +32,7 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew qa --parallel
|
run: ./gradlew qa
|
||||||
|
|
||||||
- name: Archive reports for failed build
|
- name: Archive reports for failed build
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
submodules: true
|
||||||
ref: ${{ github.event.pull_request.base.sha }}
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
@@ -37,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||||
run: ./gradlew assemblePlayProdRelease --parallel
|
run: ./gradlew assemblePlayProdRelease
|
||||||
|
|
||||||
- name: Copy base apk
|
- name: Copy base apk
|
||||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||||
@@ -45,10 +46,11 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
submodules: true
|
||||||
clean: 'false'
|
clean: 'false'
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew assemblePlayProdRelease --parallel
|
run: ./gradlew assemblePlayProdRelease
|
||||||
|
|
||||||
- name: Copy PR apk
|
- name: Copy PR apk
|
||||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
|
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ captures/
|
|||||||
project.properties
|
project.properties
|
||||||
keystore.debug.properties
|
keystore.debug.properties
|
||||||
keystore.staging.properties
|
keystore.staging.properties
|
||||||
|
nightly-url.txt
|
||||||
.project
|
.project
|
||||||
.settings
|
.settings
|
||||||
bin/
|
bin/
|
||||||
@@ -28,4 +29,4 @@ jni/libspeex/.deps/
|
|||||||
pkcs11.password
|
pkcs11.password
|
||||||
dev.keystore
|
dev.keystore
|
||||||
maps.key
|
maps.key
|
||||||
local/
|
local/
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ Truths which we believe to be self-evident:
|
|||||||
1. **There is no such thing as time.** Protocol ideas that require synchronized clocks are doomed to failure.
|
1. **There is no such thing as time.** Protocol ideas that require synchronized clocks are doomed to failure.
|
||||||
|
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
1. You'll need to get the `libwebp` submodule after checking out the repository with `git submodule init && git submodule update`
|
||||||
|
1. Most things are pretty straightforward, and opening the project in Android Studio should get you most of the way there.
|
||||||
|
1. Depending on your configuration, you'll also likely need to install additional SDK Tool components, namely the versions of NDK and CMake we are currently using in our [Docker](https://github.com/signalapp/Signal-Android/blob/main/reproducible-builds/Dockerfile#L30) configuration.
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
### Useful bug reports
|
### Useful bug reports
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Signal Android
|
# Signal Android
|
||||||
|
|
||||||
Signal is a simple, powerful, and secure messenger.
|
Signal is a simple, powerful, and secure messenger.
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ The form and manner of this distribution makes it eligible for export under the
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2013-2023 Signal
|
Copyright 2013-2024 Signal Messenger, LLC
|
||||||
|
|
||||||
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
*.db
|
|
||||||
*.db.gz
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import sys
|
|
||||||
import re
|
|
||||||
import argparse
|
|
||||||
import sqlite3
|
|
||||||
import gzip
|
|
||||||
from progressbar import ProgressBar, Counter, Timer
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog='apntool', description="""Process Android's apn xml files and drop them into an
|
|
||||||
easily queryable SQLite db. Tested up to version 9 of
|
|
||||||
their APN file.""")
|
|
||||||
parser.add_argument('-v', '--version', action='version', version='%(prog)s v1.1')
|
|
||||||
parser.add_argument('-i', '--input', help='the xml file to parse', default='apns.xml', required=False)
|
|
||||||
parser.add_argument('-o', '--output', help='the sqlite db output file', default='apns.db', required=False)
|
|
||||||
parser.add_argument('--quiet', help='do not show progress or verbose instructions', action='store_true', required=False)
|
|
||||||
parser.add_argument('--no-gzip', help="do not gzip after creation", action='store_true', required=False)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def normalized(target):
|
|
||||||
o2_typo = re.compile(r"02\.co\.uk")
|
|
||||||
port_typo = re.compile(r"(\d+\.\d+\.\d+\.\d+)\.(\d+)")
|
|
||||||
leading_zeros = re.compile(r"(/|\.|^)0+(\d+)")
|
|
||||||
subbed = o2_typo.sub(r'o2.co.uk', target)
|
|
||||||
subbed = port_typo.sub(r'\1:\2', subbed)
|
|
||||||
subbed = leading_zeros.sub(r'\1\2', subbed)
|
|
||||||
return subbed
|
|
||||||
|
|
||||||
try:
|
|
||||||
connection = sqlite3.connect(args.output)
|
|
||||||
cursor = connection.cursor()
|
|
||||||
cursor.execute('SELECT SQLITE_VERSION()')
|
|
||||||
version = cursor.fetchone()
|
|
||||||
if not args.quiet:
|
|
||||||
print("SQLite version: %s" % version)
|
|
||||||
print("Opening %s" % args.input)
|
|
||||||
|
|
||||||
cursor.execute("PRAGMA legacy_file_format=ON")
|
|
||||||
cursor.execute("PRAGMA journal_mode=DELETE")
|
|
||||||
cursor.execute("PRAGMA page_size=32768")
|
|
||||||
cursor.execute("VACUUM")
|
|
||||||
cursor.execute("DROP TABLE IF EXISTS apns")
|
|
||||||
cursor.execute("""CREATE TABLE apns(_id INTEGER PRIMARY KEY, mccmnc TEXT, mcc TEXT, mnc TEXT, carrier TEXT,
|
|
||||||
apn TEXT, mmsc TEXT, port INTEGER, type TEXT, protocol TEXT, bearer TEXT, roaming_protocol TEXT,
|
|
||||||
carrier_enabled INTEGER, mmsproxy TEXT, mmsport INTEGER, proxy TEXT, mvno_match_data TEXT,
|
|
||||||
mvno_type TEXT, authtype INTEGER, user TEXT, password TEXT, server TEXT)""")
|
|
||||||
|
|
||||||
apns = etree.parse(args.input)
|
|
||||||
root = apns.getroot()
|
|
||||||
pbar = None
|
|
||||||
if not args.quiet:
|
|
||||||
pbar = ProgressBar(widgets=['Processed: ', Counter(), ' apns (', Timer(), ')'], maxval=len(list(root))).start()
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for apn in root.iter("apn"):
|
|
||||||
if apn.get("mmsc") is None:
|
|
||||||
continue
|
|
||||||
sqlvars = ["?" for x in apn.attrib.keys()] + ["?"]
|
|
||||||
mccmnc = "%s%s" % (apn.get("mcc"), apn.get("mnc"))
|
|
||||||
normalized_mmsc = normalized(apn.get("mmsc"))
|
|
||||||
if normalized_mmsc != apn.get("mmsc"):
|
|
||||||
print("normalize MMSC: %s => %s" % (apn.get("mmsc"), normalized_mmsc))
|
|
||||||
apn.set("mmsc", normalized_mmsc)
|
|
||||||
|
|
||||||
if not apn.get("mmsproxy") is None:
|
|
||||||
normalized_mmsproxy = normalized(apn.get("mmsproxy"))
|
|
||||||
if normalized_mmsproxy != apn.get("mmsproxy"):
|
|
||||||
print("normalize proxy: %s => %s" % (apn.get("mmsproxy"), normalized_mmsproxy))
|
|
||||||
apn.set("mmsproxy", normalized_mmsproxy)
|
|
||||||
|
|
||||||
values = [apn.get(attrib) for attrib in apn.attrib.keys()] + [mccmnc]
|
|
||||||
keys = apn.attrib.keys() + ["mccmnc"]
|
|
||||||
|
|
||||||
cursor.execute("SELECT 1 FROM apns WHERE mccmnc = ? AND apn = ?", [mccmnc, apn.get("apn")])
|
|
||||||
if cursor.fetchone() is None:
|
|
||||||
statement = "INSERT INTO apns (%s) VALUES (%s)" % (", ".join(keys), ", ".join(sqlvars))
|
|
||||||
cursor.execute(statement, values)
|
|
||||||
|
|
||||||
count += 1
|
|
||||||
if not args.quiet:
|
|
||||||
pbar.update(count)
|
|
||||||
|
|
||||||
if not args.quiet:
|
|
||||||
pbar.finish()
|
|
||||||
connection.commit()
|
|
||||||
print("Successfully written to %s" % args.output)
|
|
||||||
|
|
||||||
if not args.no_gzip:
|
|
||||||
gzipped_file = "%s.gz" % (args.output,)
|
|
||||||
with open(args.output, 'rb') as orig:
|
|
||||||
with gzip.open(gzipped_file, 'wb') as gzipped:
|
|
||||||
gzipped.writelines(orig)
|
|
||||||
print("Successfully gzipped to %s" % gzipped_file)
|
|
||||||
|
|
||||||
if not args.quiet:
|
|
||||||
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.")
|
|
||||||
print("If you support API 10 or lower, you must use the gzipped version to avoid corruption.")
|
|
||||||
|
|
||||||
except sqlite3.Error as e:
|
|
||||||
if connection:
|
|
||||||
connection.rollback()
|
|
||||||
print("Error: %s" % e.args[0])
|
|
||||||
sys.exit(1)
|
|
||||||
finally:
|
|
||||||
if connection:
|
|
||||||
connection.close()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
argparse>=1.2.1
|
|
||||||
lxml>=3.3.3
|
|
||||||
progressbar-latest>=2.4
|
|
||||||
@@ -1,719 +0,0 @@
|
|||||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'kotlin-android'
|
|
||||||
id 'com.google.protobuf'
|
|
||||||
id 'androidx.navigation.safeargs'
|
|
||||||
id 'org.jlleitschuh.gradle.ktlint'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
id 'app.cash.exhaustive'
|
|
||||||
id 'kotlin-parcelize'
|
|
||||||
id 'com.squareup.wire'
|
|
||||||
id 'translations'
|
|
||||||
id 'licenses'
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: 'static-ips.gradle'
|
|
||||||
|
|
||||||
protobuf {
|
|
||||||
protoc {
|
|
||||||
artifact = 'com.google.protobuf:protoc:3.18.0'
|
|
||||||
}
|
|
||||||
generateProtoTasks {
|
|
||||||
all().each { task ->
|
|
||||||
task.builtins {
|
|
||||||
java {
|
|
||||||
option "lite"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wire {
|
|
||||||
kotlin {
|
|
||||||
javaInterop = true
|
|
||||||
}
|
|
||||||
|
|
||||||
sourcePath {
|
|
||||||
srcDir 'src/main/protowire'
|
|
||||||
}
|
|
||||||
|
|
||||||
protoPath {
|
|
||||||
srcDir "${project.rootDir}/libsignal/service/src/main/protowire"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ktlint {
|
|
||||||
version = "0.49.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
def canonicalVersionCode = 1330
|
|
||||||
def canonicalVersionName = "6.33.2"
|
|
||||||
|
|
||||||
def postFixSize = 100
|
|
||||||
def abiPostFix = ['universal' : 0,
|
|
||||||
'armeabi-v7a' : 1,
|
|
||||||
'arm64-v8a' : 2,
|
|
||||||
'x86' : 3,
|
|
||||||
'x86_64' : 4]
|
|
||||||
|
|
||||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
|
||||||
|
|
||||||
def selectableVariants = [
|
|
||||||
'nightlyProdSpinner',
|
|
||||||
'nightlyProdPerf',
|
|
||||||
'nightlyProdRelease',
|
|
||||||
'nightlyStagingRelease',
|
|
||||||
'nightlyPnpPerf',
|
|
||||||
'nightlyPnpRelease',
|
|
||||||
'playProdDebug',
|
|
||||||
'playProdSpinner',
|
|
||||||
'playProdCanary',
|
|
||||||
'playProdPerf',
|
|
||||||
'playProdBenchmark',
|
|
||||||
'playProdInstrumentation',
|
|
||||||
'playProdRelease',
|
|
||||||
'playStagingDebug',
|
|
||||||
'playStagingCanary',
|
|
||||||
'playStagingSpinner',
|
|
||||||
'playStagingPerf',
|
|
||||||
'playStagingInstrumentation',
|
|
||||||
'playPnpDebug',
|
|
||||||
'playPnpSpinner',
|
|
||||||
'playStagingRelease',
|
|
||||||
'websiteProdSpinner',
|
|
||||||
'websiteProdRelease',
|
|
||||||
]
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'org.thoughtcrime.securesms'
|
|
||||||
|
|
||||||
buildToolsVersion = signalBuildToolsVersion
|
|
||||||
compileSdkVersion = signalCompileSdkVersion
|
|
||||||
|
|
||||||
flavorDimensions 'distribution', 'environment'
|
|
||||||
useLibrary 'org.apache.http.legacy'
|
|
||||||
testBuildType 'instrumentation'
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = signalKotlinJvmTarget
|
|
||||||
freeCompilerArgs = ["-Xallow-result-return-type"]
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
if (keystores.debug != null) {
|
|
||||||
debug {
|
|
||||||
storeFile file("${project.rootDir}/${keystores.debug.storeFile}")
|
|
||||||
storePassword keystores.debug.storePassword
|
|
||||||
keyAlias keystores.debug.keyAlias
|
|
||||||
keyPassword keystores.debug.keyPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
|
||||||
|
|
||||||
unitTests {
|
|
||||||
includeAndroidResources = true
|
|
||||||
}
|
|
||||||
|
|
||||||
managedDevices {
|
|
||||||
devices {
|
|
||||||
pixel3api30 (ManagedVirtualDevice) {
|
|
||||||
device = "Pixel 3"
|
|
||||||
apiLevel = 30
|
|
||||||
systemImageSource = "google-atd"
|
|
||||||
require64Bit = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
test {
|
|
||||||
java.srcDirs += "$projectDir/src/testShared"
|
|
||||||
}
|
|
||||||
|
|
||||||
androidTest {
|
|
||||||
java.srcDirs += "$projectDir/src/testShared"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
coreLibraryDesugaringEnabled true
|
|
||||||
sourceCompatibility signalJavaVersion
|
|
||||||
targetCompatibility signalJavaVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
resources {
|
|
||||||
excludes += ['LICENSE.txt', 'LICENSE', 'NOTICE', 'asm-license.txt', 'META-INF/LICENSE', 'META-INF/LICENSE.md', 'META-INF/NOTICE', 'META-INF/LICENSE-notice.md', 'META-INF/proguard/androidx-annotations.pro', 'libsignal_jni.dylib', 'signal_jni.dll']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
compose true
|
|
||||||
}
|
|
||||||
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = '1.4.4'
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
versionCode canonicalVersionCode * postFixSize
|
|
||||||
versionName canonicalVersionName
|
|
||||||
|
|
||||||
minSdkVersion signalMinSdkVersion
|
|
||||||
targetSdkVersion signalTargetSdkVersion
|
|
||||||
|
|
||||||
multiDexEnabled true
|
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
project.ext.set("archivesBaseName", "Signal")
|
|
||||||
|
|
||||||
manifestPlaceholders = [mapsKey:"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"]
|
|
||||||
|
|
||||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
|
||||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
|
||||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
|
|
||||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN3_URL", "\"https://cdn3.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\""
|
|
||||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}"
|
|
||||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\", \"https://sfu.staging.test.voip.signal.org\"}"
|
|
||||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
|
||||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
|
||||||
buildConfigField "String[]", "SIGNAL_SERVICE_IPS", service_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_STORAGE_IPS", storage_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_CDN_IPS", cdn_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_CDN2_IPS", cdn2_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_CDN3_IPS", cdn3_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_CDSI_IPS", cdsi_ips
|
|
||||||
buildConfigField "String[]", "SIGNAL_SVR2_IPS", svr2_ips
|
|
||||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
|
||||||
buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\""
|
|
||||||
buildConfigField "String", "SVR2_MRENCLAVE", "\"6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094\""
|
|
||||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
|
|
||||||
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
|
|
||||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
|
||||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
|
|
||||||
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
|
|
||||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
|
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\""
|
|
||||||
buildConfigField "String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\""
|
|
||||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
|
||||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
|
||||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
|
||||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
|
||||||
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\""
|
|
||||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
|
||||||
|
|
||||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
|
||||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
|
|
||||||
buildConfigField "String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\""
|
|
||||||
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\""
|
|
||||||
buildConfigField "boolean", "TRACING_ENABLED", "false"
|
|
||||||
|
|
||||||
ndk {
|
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
|
||||||
}
|
|
||||||
resourceConfigurations += []
|
|
||||||
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
enable !project.hasProperty('generateBaselineProfile')
|
|
||||||
reset()
|
|
||||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
|
||||||
universalApk true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testInstrumentationRunner "org.thoughtcrime.securesms.testing.SignalTestRunner"
|
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
if (keystores['debug'] != null) {
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
}
|
|
||||||
isDefault true
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
|
||||||
'proguard/proguard-firebase-messaging.pro',
|
|
||||||
'proguard/proguard-google-play-services.pro',
|
|
||||||
'proguard/proguard-jackson.pro',
|
|
||||||
'proguard/proguard-sqlite.pro',
|
|
||||||
'proguard/proguard-appcompat-v7.pro',
|
|
||||||
'proguard/proguard-square-okhttp.pro',
|
|
||||||
'proguard/proguard-square-okio.pro',
|
|
||||||
'proguard/proguard-rounded-image-view.pro',
|
|
||||||
'proguard/proguard-glide.pro',
|
|
||||||
'proguard/proguard-shortcutbadger.pro',
|
|
||||||
'proguard/proguard-retrofit.pro',
|
|
||||||
'proguard/proguard-webrtc.pro',
|
|
||||||
'proguard/proguard-klinker.pro',
|
|
||||||
'proguard/proguard-mobilecoin.pro',
|
|
||||||
'proguard/proguard-retrolambda.pro',
|
|
||||||
'proguard/proguard-okhttp.pro',
|
|
||||||
'proguard/proguard-ez-vcard.pro',
|
|
||||||
'proguard/proguard.cfg'
|
|
||||||
testProguardFiles 'proguard/proguard-automation.pro',
|
|
||||||
'proguard/proguard.cfg'
|
|
||||||
|
|
||||||
manifestPlaceholders = [mapsKey:getMapsKey()]
|
|
||||||
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
|
||||||
}
|
|
||||||
|
|
||||||
instrumentation {
|
|
||||||
initWith debug
|
|
||||||
isDefault false
|
|
||||||
minifyEnabled false
|
|
||||||
matchingFallbacks = ['debug']
|
|
||||||
applicationIdSuffix ".instrumentation"
|
|
||||||
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Instrumentation\""
|
|
||||||
}
|
|
||||||
|
|
||||||
spinner {
|
|
||||||
initWith debug
|
|
||||||
isDefault false
|
|
||||||
minifyEnabled false
|
|
||||||
matchingFallbacks = ['debug']
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
|
|
||||||
}
|
|
||||||
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles = buildTypes.debug.proguardFiles
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
|
|
||||||
}
|
|
||||||
|
|
||||||
perf {
|
|
||||||
initWith debug
|
|
||||||
isDefault false
|
|
||||||
debuggable false
|
|
||||||
minifyEnabled true
|
|
||||||
matchingFallbacks = ['debug']
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
|
|
||||||
buildConfigField "boolean", "TRACING_ENABLED", "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
benchmark {
|
|
||||||
initWith debug
|
|
||||||
isDefault false
|
|
||||||
debuggable false
|
|
||||||
minifyEnabled true
|
|
||||||
matchingFallbacks = ['debug']
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Benchmark\""
|
|
||||||
buildConfigField "boolean", "TRACING_ENABLED", "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
canary {
|
|
||||||
initWith debug
|
|
||||||
isDefault false
|
|
||||||
minifyEnabled false
|
|
||||||
matchingFallbacks = ['debug']
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Canary\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
play {
|
|
||||||
dimension 'distribution'
|
|
||||||
isDefault true
|
|
||||||
ext.websiteUpdateUrl = "null"
|
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
|
||||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"play\""
|
|
||||||
}
|
|
||||||
|
|
||||||
website {
|
|
||||||
dimension 'distribution'
|
|
||||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
|
||||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
|
|
||||||
}
|
|
||||||
|
|
||||||
nightly {
|
|
||||||
dimension 'distribution'
|
|
||||||
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
|
|
||||||
ext.websiteUpdateUrl = "null"
|
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
|
||||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\""
|
|
||||||
}
|
|
||||||
|
|
||||||
prod {
|
|
||||||
dimension 'environment'
|
|
||||||
|
|
||||||
isDefault true
|
|
||||||
|
|
||||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\""
|
|
||||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\""
|
|
||||||
}
|
|
||||||
|
|
||||||
staging {
|
|
||||||
dimension 'environment'
|
|
||||||
|
|
||||||
applicationIdSuffix ".staging"
|
|
||||||
|
|
||||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
|
|
||||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
|
|
||||||
buildConfigField "String", "SVR2_MRENCLAVE", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\""
|
|
||||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
|
|
||||||
"\"ee1d0d972b7ea903615670de43ab1b6e7a825e811c70a29bb5fe0f819e0975fa\", " +
|
|
||||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
|
||||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
|
|
||||||
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
|
|
||||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
|
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\""
|
|
||||||
buildConfigField "String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\""
|
|
||||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
|
||||||
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\""
|
|
||||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
|
||||||
|
|
||||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
|
||||||
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\""
|
|
||||||
}
|
|
||||||
|
|
||||||
pnp {
|
|
||||||
dimension 'environment'
|
|
||||||
|
|
||||||
initWith staging
|
|
||||||
applicationIdSuffix ".pnp"
|
|
||||||
|
|
||||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Pnp\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
|
||||||
abortOnError true
|
|
||||||
baseline file('lint-baseline.xml')
|
|
||||||
checkReleaseBuilds false
|
|
||||||
disable 'LintError'
|
|
||||||
}
|
|
||||||
|
|
||||||
android.applicationVariants.all { variant ->
|
|
||||||
variant.outputs.each { output ->
|
|
||||||
if (output.baseName.contains('nightly')) {
|
|
||||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
|
|
||||||
def tag = getCurrentGitTag()
|
|
||||||
if (tag != null && tag.length() > 0) {
|
|
||||||
if (tag.startsWith("v")) {
|
|
||||||
tag = tag.substring(1)
|
|
||||||
}
|
|
||||||
output.versionNameOverride = tag
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
|
||||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
|
||||||
def postFix = abiPostFix.get(abiName, 0)
|
|
||||||
|
|
||||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
|
||||||
|
|
||||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android.variantFilter { variant ->
|
|
||||||
def distribution = variant.getFlavors().get(0).name
|
|
||||||
def environment = variant.getFlavors().get(1).name
|
|
||||||
def buildType = variant.buildType.name
|
|
||||||
def fullName = distribution + environment.capitalize() + buildType.capitalize()
|
|
||||||
|
|
||||||
if (!selectableVariants.contains(fullName)) {
|
|
||||||
variant.setIgnore(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android.buildTypes.each {
|
|
||||||
if (it.name != 'release') {
|
|
||||||
sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/debug/java"
|
|
||||||
} else {
|
|
||||||
sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/release/java"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation libs.androidx.fragment.ktx
|
|
||||||
lintChecks project(':lintchecks')
|
|
||||||
|
|
||||||
coreLibraryDesugaring libs.android.tools.desugar
|
|
||||||
|
|
||||||
implementation (libs.androidx.appcompat) {
|
|
||||||
version {
|
|
||||||
strictly '1.6.1'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation libs.androidx.window.window
|
|
||||||
implementation libs.androidx.window.java
|
|
||||||
implementation libs.androidx.recyclerview
|
|
||||||
implementation libs.material.material
|
|
||||||
implementation libs.androidx.legacy.support
|
|
||||||
implementation libs.androidx.preference
|
|
||||||
implementation libs.androidx.legacy.preference
|
|
||||||
implementation libs.androidx.gridlayout
|
|
||||||
implementation libs.androidx.exifinterface
|
|
||||||
implementation libs.androidx.compose.rxjava3
|
|
||||||
implementation libs.androidx.compose.runtime.livedata
|
|
||||||
implementation libs.androidx.constraintlayout
|
|
||||||
implementation libs.androidx.multidex
|
|
||||||
implementation libs.androidx.navigation.fragment.ktx
|
|
||||||
implementation libs.androidx.navigation.ui.ktx
|
|
||||||
implementation libs.androidx.lifecycle.viewmodel.ktx
|
|
||||||
implementation libs.androidx.lifecycle.livedata.ktx
|
|
||||||
implementation libs.androidx.lifecycle.process
|
|
||||||
implementation libs.androidx.lifecycle.viewmodel.savedstate
|
|
||||||
implementation libs.androidx.lifecycle.common.java8
|
|
||||||
implementation libs.androidx.lifecycle.reactivestreams.ktx
|
|
||||||
implementation libs.androidx.camera.core
|
|
||||||
implementation libs.androidx.camera.camera2
|
|
||||||
implementation libs.androidx.camera.lifecycle
|
|
||||||
implementation libs.androidx.camera.view
|
|
||||||
implementation libs.androidx.concurrent.futures
|
|
||||||
implementation libs.androidx.autofill
|
|
||||||
implementation libs.androidx.biometric
|
|
||||||
implementation libs.androidx.sharetarget
|
|
||||||
implementation libs.androidx.profileinstaller
|
|
||||||
implementation libs.androidx.asynclayoutinflater
|
|
||||||
implementation libs.androidx.asynclayoutinflater.appcompat
|
|
||||||
|
|
||||||
implementation (libs.firebase.messaging) {
|
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation libs.google.play.services.maps
|
|
||||||
implementation libs.google.play.services.auth
|
|
||||||
|
|
||||||
implementation libs.bundles.media3
|
|
||||||
|
|
||||||
implementation libs.conscrypt.android
|
|
||||||
implementation libs.signal.aesgcmprovider
|
|
||||||
|
|
||||||
implementation project(':libsignal-service')
|
|
||||||
implementation project(':paging')
|
|
||||||
implementation project(':core-util')
|
|
||||||
implementation project(':glide-config')
|
|
||||||
implementation project(':video')
|
|
||||||
implementation project(':device-transfer')
|
|
||||||
implementation project(':image-editor')
|
|
||||||
implementation project(':donations')
|
|
||||||
implementation project(':contacts')
|
|
||||||
implementation project(':qr')
|
|
||||||
implementation project(':sms-exporter')
|
|
||||||
implementation project(':sticky-header-grid')
|
|
||||||
implementation project(':photoview')
|
|
||||||
|
|
||||||
implementation libs.libsignal.android
|
|
||||||
implementation libs.google.protobuf.javalite
|
|
||||||
|
|
||||||
implementation(libs.mobilecoin) {
|
|
||||||
exclude group: 'com.google.protobuf'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation libs.signal.ringrtc
|
|
||||||
|
|
||||||
implementation libs.leolin.shortcutbadger
|
|
||||||
implementation libs.emilsjolander.stickylistheaders
|
|
||||||
implementation libs.apache.httpclient.android
|
|
||||||
implementation libs.glide.glide
|
|
||||||
implementation libs.roundedimageview
|
|
||||||
implementation libs.materialish.progress
|
|
||||||
implementation libs.greenrobot.eventbus
|
|
||||||
implementation libs.google.zxing.android.integration
|
|
||||||
implementation libs.google.zxing.core
|
|
||||||
implementation libs.google.flexbox
|
|
||||||
implementation (libs.subsampling.scale.image.view) {
|
|
||||||
exclude group: 'com.android.support', module: 'support-annotations'
|
|
||||||
}
|
|
||||||
implementation (libs.android.tooltips) {
|
|
||||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
|
||||||
}
|
|
||||||
implementation (libs.android.smsmms) {
|
|
||||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
|
||||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
|
||||||
}
|
|
||||||
implementation libs.stream
|
|
||||||
|
|
||||||
implementation libs.lottie
|
|
||||||
|
|
||||||
implementation libs.signal.android.database.sqlcipher
|
|
||||||
implementation libs.androidx.sqlite
|
|
||||||
|
|
||||||
implementation (libs.google.ez.vcard) {
|
|
||||||
exclude group: 'com.fasterxml.jackson.core'
|
|
||||||
exclude group: 'org.freemarker'
|
|
||||||
}
|
|
||||||
implementation libs.dnsjava
|
|
||||||
implementation libs.kotlinx.collections.immutable
|
|
||||||
implementation libs.accompanist.permissions
|
|
||||||
|
|
||||||
spinnerImplementation project(":spinner")
|
|
||||||
|
|
||||||
canaryImplementation libs.square.leakcanary
|
|
||||||
|
|
||||||
testImplementation testLibs.junit.junit
|
|
||||||
testImplementation testLibs.assertj.core
|
|
||||||
testImplementation testLibs.mockito.core
|
|
||||||
testImplementation testLibs.mockito.kotlin
|
|
||||||
|
|
||||||
testImplementation testLibs.androidx.test.core
|
|
||||||
testImplementation (testLibs.robolectric.robolectric) {
|
|
||||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
|
||||||
}
|
|
||||||
testImplementation testLibs.robolectric.shadows.multidex
|
|
||||||
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
|
|
||||||
testImplementation (testLibs.bouncycastle.bcpkix.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
|
|
||||||
testImplementation testLibs.conscrypt.openjdk.uber // Used by robolectric
|
|
||||||
testImplementation testLibs.hamcrest.hamcrest
|
|
||||||
testImplementation testLibs.mockk
|
|
||||||
|
|
||||||
testImplementation(testFixtures(project(":libsignal-service")))
|
|
||||||
|
|
||||||
androidTestImplementation testLibs.androidx.test.ext.junit
|
|
||||||
androidTestImplementation testLibs.espresso.core
|
|
||||||
androidTestImplementation testLibs.androidx.test.core
|
|
||||||
androidTestImplementation testLibs.androidx.test.core.ktx
|
|
||||||
androidTestImplementation testLibs.androidx.test.ext.junit.ktx
|
|
||||||
androidTestImplementation testLibs.mockito.android
|
|
||||||
androidTestImplementation testLibs.mockito.kotlin
|
|
||||||
androidTestImplementation testLibs.mockk.android
|
|
||||||
androidTestImplementation testLibs.square.okhttp.mockserver
|
|
||||||
|
|
||||||
instrumentationImplementation (libs.androidx.fragment.testing) {
|
|
||||||
exclude group: 'androidx.test', module: 'core'
|
|
||||||
}
|
|
||||||
|
|
||||||
testImplementation testLibs.espresso.core
|
|
||||||
|
|
||||||
implementation libs.kotlin.stdlib.jdk8
|
|
||||||
implementation libs.kotlin.reflect
|
|
||||||
implementation libs.jackson.module.kotlin
|
|
||||||
|
|
||||||
implementation libs.rxjava3.rxandroid
|
|
||||||
implementation libs.rxjava3.rxkotlin
|
|
||||||
implementation libs.rxdogtag
|
|
||||||
|
|
||||||
androidTestUtil testLibs.androidx.test.orchestrator
|
|
||||||
|
|
||||||
implementation project(':core-ui')
|
|
||||||
ktlintRuleset libs.ktlint.twitter.compose
|
|
||||||
}
|
|
||||||
|
|
||||||
def getLastCommitTimestamp() {
|
|
||||||
if (!(new File('.git').exists())) {
|
|
||||||
return System.currentTimeMillis().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
new ByteArrayOutputStream().withStream { os ->
|
|
||||||
exec {
|
|
||||||
executable = 'git'
|
|
||||||
args = ['log', '-1', '--pretty=format:%ct']
|
|
||||||
standardOutput = os
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.toString() + "000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getGitHash() {
|
|
||||||
if (!(new File('.git').exists())) {
|
|
||||||
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
|
||||||
}
|
|
||||||
|
|
||||||
def stdout = new ByteArrayOutputStream()
|
|
||||||
exec {
|
|
||||||
commandLine 'git', 'rev-parse', 'HEAD'
|
|
||||||
standardOutput = stdout
|
|
||||||
}
|
|
||||||
return stdout.toString().trim().substring(0, 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
def getCurrentGitTag() {
|
|
||||||
if (!(new File('.git').exists())) {
|
|
||||||
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
|
||||||
}
|
|
||||||
|
|
||||||
def stdout = new ByteArrayOutputStream()
|
|
||||||
exec {
|
|
||||||
commandLine 'git', 'tag', '--points-at', 'HEAD'
|
|
||||||
standardOutput = stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
def output = stdout.toString().trim()
|
|
||||||
|
|
||||||
if (output != null && output.size() > 0) {
|
|
||||||
def tags = output.split('\n').toList()
|
|
||||||
return tags.stream().filter(t -> t.contains('nightly')).findFirst().orElse(tags.get(0))
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(Test) {
|
|
||||||
testLogging {
|
|
||||||
events "failed"
|
|
||||||
exceptionFormat "full"
|
|
||||||
showCauses true
|
|
||||||
showExceptions true
|
|
||||||
showStackTraces true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def loadKeystoreProperties(filename) {
|
|
||||||
def keystorePropertiesFile = file("${project.rootDir}/${filename}")
|
|
||||||
if (keystorePropertiesFile.exists()) {
|
|
||||||
def keystoreProperties = new Properties()
|
|
||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
|
||||||
return keystoreProperties
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static def getDateSuffix() {
|
|
||||||
def date = new Date()
|
|
||||||
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
|
|
||||||
return formattedDate
|
|
||||||
}
|
|
||||||
|
|
||||||
def getMapsKey() {
|
|
||||||
def mapKey = file("${project.rootDir}/maps.key")
|
|
||||||
if (mapKey.exists()) {
|
|
||||||
return mapKey.readLines()[0]
|
|
||||||
}
|
|
||||||
return "AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,715 @@
|
|||||||
|
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||||
|
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
id("androidx.navigation.safeargs")
|
||||||
|
id("org.jlleitschuh.gradle.ktlint")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("app.cash.exhaustive")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
id("com.squareup.wire")
|
||||||
|
id("translations")
|
||||||
|
id("licenses")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "static-ips.gradle.kts")
|
||||||
|
|
||||||
|
val canonicalVersionCode = 1433
|
||||||
|
val canonicalVersionName = "7.11.0"
|
||||||
|
val currentHotfixVersion = 0
|
||||||
|
val maxHotfixVersions = 100
|
||||||
|
|
||||||
|
val keystores: Map<String, Properties?> = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
|
||||||
|
|
||||||
|
val selectableVariants = listOf(
|
||||||
|
"nightlyProdSpinner",
|
||||||
|
"nightlyProdPerf",
|
||||||
|
"nightlyProdRelease",
|
||||||
|
"nightlyStagingRelease",
|
||||||
|
"playProdDebug",
|
||||||
|
"playProdSpinner",
|
||||||
|
"playProdCanary",
|
||||||
|
"playProdPerf",
|
||||||
|
"playProdBenchmark",
|
||||||
|
"playProdInstrumentation",
|
||||||
|
"playProdRelease",
|
||||||
|
"playStagingDebug",
|
||||||
|
"playStagingCanary",
|
||||||
|
"playStagingSpinner",
|
||||||
|
"playStagingPerf",
|
||||||
|
"playStagingInstrumentation",
|
||||||
|
"playStagingRelease",
|
||||||
|
"websiteProdSpinner",
|
||||||
|
"websiteProdRelease"
|
||||||
|
)
|
||||||
|
|
||||||
|
val signalBuildToolsVersion: String by rootProject.extra
|
||||||
|
val signalCompileSdkVersion: String by rootProject.extra
|
||||||
|
val signalTargetSdkVersion: Int by rootProject.extra
|
||||||
|
val signalMinSdkVersion: Int by rootProject.extra
|
||||||
|
val signalJavaVersion: JavaVersion by rootProject.extra
|
||||||
|
val signalKotlinJvmTarget: String by rootProject.extra
|
||||||
|
|
||||||
|
wire {
|
||||||
|
kotlin {
|
||||||
|
javaInterop = true
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath {
|
||||||
|
srcDir("src/main/protowire")
|
||||||
|
}
|
||||||
|
|
||||||
|
protoPath {
|
||||||
|
srcDir("${project.rootDir}/libsignal-service/src/main/protowire")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ktlint {
|
||||||
|
version.set("1.2.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "org.thoughtcrime.securesms"
|
||||||
|
|
||||||
|
buildToolsVersion = signalBuildToolsVersion
|
||||||
|
compileSdkVersion = signalCompileSdkVersion
|
||||||
|
|
||||||
|
flavorDimensions += listOf("distribution", "environment")
|
||||||
|
useLibrary("org.apache.http.legacy")
|
||||||
|
testBuildType = "instrumentation"
|
||||||
|
|
||||||
|
android.bundle.language.enableSplit = false
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = signalKotlinJvmTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
keystores["debug"]?.let { properties ->
|
||||||
|
signingConfigs.getByName("debug").apply {
|
||||||
|
storeFile = file("${project.rootDir}/${properties.getProperty("storeFile")}")
|
||||||
|
storePassword = properties.getProperty("storePassword")
|
||||||
|
keyAlias = properties.getProperty("keyAlias")
|
||||||
|
keyPassword = properties.getProperty("keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||||
|
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
|
||||||
|
managedDevices {
|
||||||
|
devices {
|
||||||
|
create<ManagedVirtualDevice>("pixel3api30") {
|
||||||
|
device = "Pixel 3"
|
||||||
|
apiLevel = 30
|
||||||
|
systemImageSource = "google-atd"
|
||||||
|
require64Bit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("test") {
|
||||||
|
java.srcDir("$projectDir/src/testShared")
|
||||||
|
}
|
||||||
|
|
||||||
|
getByName("androidTest") {
|
||||||
|
java.srcDir("$projectDir/src/testShared")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
sourceCompatibility = signalJavaVersion
|
||||||
|
targetCompatibility = signalJavaVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += setOf("LICENSE.txt", "LICENSE", "NOTICE", "asm-license.txt", "META-INF/LICENSE", "META-INF/LICENSE.md", "META-INF/NOTICE", "META-INF/LICENSE-notice.md", "META-INF/proguard/androidx-annotations.pro", "libsignal_jni.dylib", "signal_jni.dll")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.5.4"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
versionCode = (canonicalVersionCode * maxHotfixVersions) + currentHotfixVersion
|
||||||
|
versionName = canonicalVersionName
|
||||||
|
|
||||||
|
minSdk = signalMinSdkVersion
|
||||||
|
targetSdk = signalTargetSdkVersion
|
||||||
|
|
||||||
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
project.ext.set("archivesBaseName", "Signal")
|
||||||
|
|
||||||
|
manifestPlaceholders["mapsKey"] = "AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L")
|
||||||
|
buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
|
||||||
|
buildConfigField("String", "SIGNAL_URL", "\"https://chat.signal.org\"")
|
||||||
|
buildConfigField("String", "STORAGE_URL", "\"https://storage.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\"")
|
||||||
|
buildConfigField("String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}")
|
||||||
|
buildConfigField("String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\", \"https://sfu.staging.test.voip.signal.org\"}")
|
||||||
|
buildConfigField("String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"")
|
||||||
|
buildConfigField("int", "CONTENT_PROXY_PORT", "443")
|
||||||
|
buildConfigField("String[]", "SIGNAL_SERVICE_IPS", rootProject.extra["service_ips"] as String)
|
||||||
|
buildConfigField("String[]", "SIGNAL_STORAGE_IPS", rootProject.extra["storage_ips"] as String)
|
||||||
|
buildConfigField("String[]", "SIGNAL_CDN_IPS", rootProject.extra["cdn_ips"] as String)
|
||||||
|
buildConfigField("String[]", "SIGNAL_CDN2_IPS", rootProject.extra["cdn2_ips"] as String)
|
||||||
|
buildConfigField("String[]", "SIGNAL_CDN3_IPS", rootProject.extra["cdn3_ips"] as String)
|
||||||
|
buildConfigField("String[]", "SIGNAL_SFU_IPS", rootProject.extra["sfu_ips"] as String)
|
||||||
|
buildConfigField("String[]", "SIGNAL_CONTENT_PROXY_IPS", rootProject.extra["content_proxy_ips"] as String)
|
||||||
|
buildConfigField("String[]", "SIGNAL_CDSI_IPS", rootProject.extra["cdsi_ips"] as String)
|
||||||
|
buildConfigField("String[]", "SIGNAL_SVR2_IPS", rootProject.extra["svr2_ips"] as String)
|
||||||
|
buildConfigField("String", "SIGNAL_AGENT", "\"OWA\"")
|
||||||
|
buildConfigField("String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\"")
|
||||||
|
buildConfigField("String", "SVR2_MRENCLAVE", "\"a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97\"")
|
||||||
|
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
|
||||||
|
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
|
||||||
|
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
|
||||||
|
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O\"")
|
||||||
|
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().map { "\"$it\"" }.joinToString(separator = ", ")} }")
|
||||||
|
buildConfigField("int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode")
|
||||||
|
buildConfigField("String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\"")
|
||||||
|
buildConfigField("String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\"")
|
||||||
|
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"")
|
||||||
|
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.PRODUCTION")
|
||||||
|
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.INFO")
|
||||||
|
|
||||||
|
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"")
|
||||||
|
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
|
||||||
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"unset\"")
|
||||||
|
buildConfigField("String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"")
|
||||||
|
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
|
||||||
|
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
||||||
|
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
|
}
|
||||||
|
resourceConfigurations += listOf()
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
isEnable = !project.hasProperty("generateBaselineProfile")
|
||||||
|
reset()
|
||||||
|
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
|
isUniversalApk = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testInstrumentationRunner = "org.thoughtcrime.securesms.testing.SignalTestRunner"
|
||||||
|
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
if (keystores["debug"] != null) {
|
||||||
|
signingConfig = signingConfigs["debug"]
|
||||||
|
}
|
||||||
|
isDefault = true
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android.txt"),
|
||||||
|
"proguard/proguard-firebase-messaging.pro",
|
||||||
|
"proguard/proguard-google-play-services.pro",
|
||||||
|
"proguard/proguard-jackson.pro",
|
||||||
|
"proguard/proguard-sqlite.pro",
|
||||||
|
"proguard/proguard-appcompat-v7.pro",
|
||||||
|
"proguard/proguard-square-okhttp.pro",
|
||||||
|
"proguard/proguard-square-okio.pro",
|
||||||
|
"proguard/proguard-rounded-image-view.pro",
|
||||||
|
"proguard/proguard-glide.pro",
|
||||||
|
"proguard/proguard-shortcutbadger.pro",
|
||||||
|
"proguard/proguard-retrofit.pro",
|
||||||
|
"proguard/proguard-webrtc.pro",
|
||||||
|
"proguard/proguard-klinker.pro",
|
||||||
|
"proguard/proguard-mobilecoin.pro",
|
||||||
|
"proguard/proguard-retrolambda.pro",
|
||||||
|
"proguard/proguard-okhttp.pro",
|
||||||
|
"proguard/proguard-ez-vcard.pro",
|
||||||
|
"proguard/proguard.cfg"
|
||||||
|
)
|
||||||
|
testProguardFiles(
|
||||||
|
"proguard/proguard-automation.pro",
|
||||||
|
"proguard/proguard.cfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
manifestPlaceholders["mapsKey"] = getMapsKey()
|
||||||
|
|
||||||
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Debug\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
getByName("release") {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(*buildTypes["debug"].proguardFiles.toTypedArray())
|
||||||
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Release\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("instrumentation") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
isDefault = false
|
||||||
|
isMinifyEnabled = false
|
||||||
|
matchingFallbacks += "debug"
|
||||||
|
applicationIdSuffix = ".instrumentation"
|
||||||
|
|
||||||
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Instrumentation\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("spinner") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
isDefault = false
|
||||||
|
isMinifyEnabled = false
|
||||||
|
matchingFallbacks += "debug"
|
||||||
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Spinner\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("perf") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
isDefault = false
|
||||||
|
isDebuggable = false
|
||||||
|
isMinifyEnabled = true
|
||||||
|
matchingFallbacks += "debug"
|
||||||
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Perf\"")
|
||||||
|
buildConfigField("boolean", "TRACING_ENABLED", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("benchmark") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
isDefault = false
|
||||||
|
isDebuggable = false
|
||||||
|
isMinifyEnabled = true
|
||||||
|
matchingFallbacks += "debug"
|
||||||
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Benchmark\"")
|
||||||
|
buildConfigField("boolean", "TRACING_ENABLED", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("canary") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
isDefault = false
|
||||||
|
isMinifyEnabled = false
|
||||||
|
matchingFallbacks += "debug"
|
||||||
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Canary\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
create("play") {
|
||||||
|
dimension = "distribution"
|
||||||
|
isDefault = true
|
||||||
|
buildConfigField("boolean", "MANAGES_APP_UPDATES", "false")
|
||||||
|
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "null")
|
||||||
|
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"play\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("website") {
|
||||||
|
dimension = "distribution"
|
||||||
|
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
|
||||||
|
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "\"https://updates.signal.org/android/latest.json\"")
|
||||||
|
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"website\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("nightly") {
|
||||||
|
val apkUpdateManifestUrl = if (file("${project.rootDir}/nightly-url.txt").exists()) {
|
||||||
|
file("${project.rootDir}/nightly-url.txt").readText().trim()
|
||||||
|
} else {
|
||||||
|
"<unset>"
|
||||||
|
}
|
||||||
|
|
||||||
|
dimension = "distribution"
|
||||||
|
versionNameSuffix = "-nightly-untagged-${getDateSuffix()}"
|
||||||
|
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
|
||||||
|
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "\"${apkUpdateManifestUrl}\"")
|
||||||
|
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("prod") {
|
||||||
|
dimension = "environment"
|
||||||
|
|
||||||
|
isDefault = true
|
||||||
|
|
||||||
|
buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\"")
|
||||||
|
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("staging") {
|
||||||
|
dimension = "environment"
|
||||||
|
|
||||||
|
applicationIdSuffix = ".staging"
|
||||||
|
|
||||||
|
buildConfigField("String", "SIGNAL_URL", "\"https://chat.staging.signal.org\"")
|
||||||
|
buildConfigField("String", "STORAGE_URL", "\"https://storage-staging.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
|
||||||
|
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
|
||||||
|
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
|
||||||
|
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
|
||||||
|
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
|
||||||
|
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")
|
||||||
|
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8\"")
|
||||||
|
buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"")
|
||||||
|
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"")
|
||||||
|
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"")
|
||||||
|
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.STAGING")
|
||||||
|
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.DEBUG")
|
||||||
|
|
||||||
|
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
|
||||||
|
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
|
||||||
|
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
abortOnError = true
|
||||||
|
baseline = file("lint-baseline.xml")
|
||||||
|
checkReleaseBuilds = false
|
||||||
|
disable += "LintError"
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all {
|
||||||
|
outputs
|
||||||
|
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||||
|
.forEach { output ->
|
||||||
|
if (output.baseName.contains("nightly")) {
|
||||||
|
var tag = getCurrentGitTag()
|
||||||
|
if (!tag.isNullOrEmpty()) {
|
||||||
|
if (tag.startsWith("v")) {
|
||||||
|
tag = tag.substring(1)
|
||||||
|
}
|
||||||
|
output.versionNameOverride = tag
|
||||||
|
output.outputFileName = output.outputFileName.replace(".apk", "-${output.versionNameOverride}.apk")
|
||||||
|
} else {
|
||||||
|
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
|
||||||
|
|
||||||
|
if (currentHotfixVersion >= maxHotfixVersions) {
|
||||||
|
throw AssertionError("Hotfix version is too large!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
beforeVariants { variant ->
|
||||||
|
variant.enable = variant.name in selectableVariants
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseDir = "$projectDir/src/release/java"
|
||||||
|
val debugDir = "$projectDir/src/debug/java"
|
||||||
|
|
||||||
|
android.buildTypes.configureEach {
|
||||||
|
val path = if (name == "release") releaseDir else debugDir
|
||||||
|
sourceSets.named(name) {
|
||||||
|
java.srcDir(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
lintChecks(project(":lintchecks"))
|
||||||
|
ktlintRuleset(libs.ktlint.twitter.compose)
|
||||||
|
coreLibraryDesugaring(libs.android.tools.desugar)
|
||||||
|
|
||||||
|
implementation(project(":libsignal-service"))
|
||||||
|
implementation(project(":paging"))
|
||||||
|
implementation(project(":core-util"))
|
||||||
|
implementation(project(":glide-config"))
|
||||||
|
implementation(project(":video"))
|
||||||
|
implementation(project(":device-transfer"))
|
||||||
|
implementation(project(":image-editor"))
|
||||||
|
implementation(project(":donations"))
|
||||||
|
implementation(project(":contacts"))
|
||||||
|
implementation(project(":qr"))
|
||||||
|
implementation(project(":sticky-header-grid"))
|
||||||
|
implementation(project(":photoview"))
|
||||||
|
implementation(project(":core-ui"))
|
||||||
|
|
||||||
|
implementation(libs.androidx.fragment.ktx)
|
||||||
|
implementation(libs.androidx.appcompat) {
|
||||||
|
version {
|
||||||
|
strictly("1.6.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
implementation(libs.androidx.window.window)
|
||||||
|
implementation(libs.androidx.window.java)
|
||||||
|
implementation(libs.androidx.recyclerview)
|
||||||
|
implementation(libs.material.material)
|
||||||
|
implementation(libs.androidx.legacy.support)
|
||||||
|
implementation(libs.androidx.preference)
|
||||||
|
implementation(libs.androidx.legacy.preference)
|
||||||
|
implementation(libs.androidx.gridlayout)
|
||||||
|
implementation(libs.androidx.exifinterface)
|
||||||
|
implementation(libs.androidx.compose.rxjava3)
|
||||||
|
implementation(libs.androidx.compose.runtime.livedata)
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.androidx.multidex)
|
||||||
|
implementation(libs.androidx.navigation.fragment.ktx)
|
||||||
|
implementation(libs.androidx.navigation.ui.ktx)
|
||||||
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
|
||||||
|
implementation(libs.androidx.lifecycle.common.java8)
|
||||||
|
implementation(libs.androidx.lifecycle.reactivestreams.ktx)
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.androidx.camera.core)
|
||||||
|
implementation(libs.androidx.camera.camera2)
|
||||||
|
implementation(libs.androidx.camera.extensions)
|
||||||
|
implementation(libs.androidx.camera.lifecycle)
|
||||||
|
implementation(libs.androidx.camera.view)
|
||||||
|
implementation(libs.androidx.concurrent.futures)
|
||||||
|
implementation(libs.androidx.autofill)
|
||||||
|
implementation(libs.androidx.biometric)
|
||||||
|
implementation(libs.androidx.sharetarget)
|
||||||
|
implementation(libs.androidx.profileinstaller)
|
||||||
|
implementation(libs.androidx.asynclayoutinflater)
|
||||||
|
implementation(libs.androidx.asynclayoutinflater.appcompat)
|
||||||
|
implementation(libs.androidx.emoji2)
|
||||||
|
implementation(libs.firebase.messaging) {
|
||||||
|
exclude(group = "com.google.firebase", module = "firebase-core")
|
||||||
|
exclude(group = "com.google.firebase", module = "firebase-analytics")
|
||||||
|
exclude(group = "com.google.firebase", module = "firebase-measurement-connector")
|
||||||
|
}
|
||||||
|
implementation(libs.google.play.services.maps)
|
||||||
|
implementation(libs.google.play.services.auth)
|
||||||
|
implementation(libs.bundles.media3)
|
||||||
|
implementation(libs.conscrypt.android)
|
||||||
|
implementation(libs.signal.aesgcmprovider)
|
||||||
|
implementation(libs.libsignal.android)
|
||||||
|
implementation(libs.mobilecoin)
|
||||||
|
implementation(libs.signal.ringrtc)
|
||||||
|
implementation(libs.leolin.shortcutbadger)
|
||||||
|
implementation(libs.emilsjolander.stickylistheaders)
|
||||||
|
implementation(libs.apache.httpclient.android)
|
||||||
|
implementation(libs.glide.glide)
|
||||||
|
implementation(libs.roundedimageview)
|
||||||
|
implementation(libs.materialish.progress)
|
||||||
|
implementation(libs.greenrobot.eventbus)
|
||||||
|
implementation(libs.google.zxing.android.integration)
|
||||||
|
implementation(libs.google.zxing.core)
|
||||||
|
implementation(libs.google.flexbox)
|
||||||
|
implementation(libs.subsampling.scale.image.view) {
|
||||||
|
exclude(group = "com.android.support", module = "support-annotations")
|
||||||
|
}
|
||||||
|
implementation(libs.android.tooltips) {
|
||||||
|
exclude(group = "com.android.support", module = "appcompat-v7")
|
||||||
|
}
|
||||||
|
implementation(libs.stream)
|
||||||
|
implementation(libs.lottie)
|
||||||
|
implementation(libs.lottie.compose)
|
||||||
|
implementation(libs.signal.android.database.sqlcipher)
|
||||||
|
implementation(libs.androidx.sqlite)
|
||||||
|
implementation(libs.google.ez.vcard) {
|
||||||
|
exclude(group = "com.fasterxml.jackson.core")
|
||||||
|
exclude(group = "org.freemarker")
|
||||||
|
}
|
||||||
|
implementation(libs.dnsjava)
|
||||||
|
implementation(libs.kotlinx.collections.immutable)
|
||||||
|
implementation(libs.accompanist.permissions)
|
||||||
|
implementation(libs.kotlin.stdlib.jdk8)
|
||||||
|
implementation(libs.kotlin.reflect)
|
||||||
|
implementation(libs.kotlinx.coroutines.play.services)
|
||||||
|
implementation(libs.jackson.module.kotlin)
|
||||||
|
implementation(libs.rxjava3.rxandroid)
|
||||||
|
implementation(libs.rxjava3.rxkotlin)
|
||||||
|
implementation(libs.rxdogtag)
|
||||||
|
|
||||||
|
"spinnerImplementation"(project(":spinner"))
|
||||||
|
|
||||||
|
"canaryImplementation"(libs.square.leakcanary)
|
||||||
|
|
||||||
|
"instrumentationImplementation"(libs.androidx.fragment.testing) {
|
||||||
|
exclude(group = "androidx.test", module = "core")
|
||||||
|
}
|
||||||
|
|
||||||
|
testImplementation(testLibs.junit.junit)
|
||||||
|
testImplementation(testLibs.assertj.core)
|
||||||
|
testImplementation(testLibs.mockito.core)
|
||||||
|
testImplementation(testLibs.mockito.kotlin)
|
||||||
|
testImplementation(testLibs.androidx.test.core)
|
||||||
|
testImplementation(testLibs.robolectric.robolectric) {
|
||||||
|
exclude(group = "com.google.protobuf", module = "protobuf-java")
|
||||||
|
}
|
||||||
|
testImplementation(testLibs.robolectric.shadows.multidex)
|
||||||
|
testImplementation(testLibs.bouncycastle.bcprov.jdk15on) {
|
||||||
|
version {
|
||||||
|
strictly("1.70")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testImplementation(testLibs.bouncycastle.bcpkix.jdk15on) {
|
||||||
|
version {
|
||||||
|
strictly("1.70")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testImplementation(testLibs.conscrypt.openjdk.uber)
|
||||||
|
testImplementation(testLibs.hamcrest.hamcrest)
|
||||||
|
testImplementation(testLibs.mockk)
|
||||||
|
testImplementation(testFixtures(project(":libsignal-service")))
|
||||||
|
testImplementation(testLibs.espresso.core)
|
||||||
|
|
||||||
|
androidTestImplementation(testLibs.androidx.test.ext.junit)
|
||||||
|
androidTestImplementation(testLibs.espresso.core)
|
||||||
|
androidTestImplementation(testLibs.androidx.test.core)
|
||||||
|
androidTestImplementation(testLibs.androidx.test.core.ktx)
|
||||||
|
androidTestImplementation(testLibs.androidx.test.ext.junit.ktx)
|
||||||
|
androidTestImplementation(testLibs.mockito.android)
|
||||||
|
androidTestImplementation(testLibs.mockito.kotlin)
|
||||||
|
androidTestImplementation(testLibs.mockk.android)
|
||||||
|
androidTestImplementation(testLibs.square.okhttp.mockserver)
|
||||||
|
|
||||||
|
androidTestUtil(testLibs.androidx.test.orchestrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertIsGitRepo() {
|
||||||
|
if (!file("${project.rootDir}/.git").exists()) {
|
||||||
|
throw IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastCommitTimestamp(): String {
|
||||||
|
assertIsGitRepo()
|
||||||
|
|
||||||
|
ByteArrayOutputStream().use { os ->
|
||||||
|
exec {
|
||||||
|
executable = "git"
|
||||||
|
args = listOf("log", "-1", "--pretty=format:%ct")
|
||||||
|
standardOutput = os
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.toString() + "000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGitHash(): String {
|
||||||
|
assertIsGitRepo()
|
||||||
|
|
||||||
|
val stdout = ByteArrayOutputStream()
|
||||||
|
exec {
|
||||||
|
commandLine = listOf("git", "rev-parse", "HEAD")
|
||||||
|
standardOutput = stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout.toString().trim().substring(0, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentGitTag(): String? {
|
||||||
|
assertIsGitRepo()
|
||||||
|
|
||||||
|
val stdout = ByteArrayOutputStream()
|
||||||
|
exec {
|
||||||
|
commandLine = listOf("git", "tag", "--points-at", "HEAD")
|
||||||
|
standardOutput = stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
val output: String = stdout.toString().trim()
|
||||||
|
|
||||||
|
return if (output.isNotEmpty()) {
|
||||||
|
val tags = output.split("\n").toList()
|
||||||
|
tags.firstOrNull { it.contains("nightly") } ?: tags[0]
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
testLogging {
|
||||||
|
events("failed")
|
||||||
|
exceptionFormat = TestExceptionFormat.FULL
|
||||||
|
showCauses = true
|
||||||
|
showExceptions = true
|
||||||
|
showStackTraces = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.tasks.configureEach {
|
||||||
|
if (name.lowercase().contains("nightly") && name != "checkNightlyParams") {
|
||||||
|
dependsOn(tasks.getByName("checkNightlyParams"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("checkNightlyParams") {
|
||||||
|
doFirst {
|
||||||
|
if (project.gradle.startParameter.taskNames.any { it.lowercase().contains("nightly") }) {
|
||||||
|
|
||||||
|
if (!file("${project.rootDir}/nightly-url.txt").exists()) {
|
||||||
|
throw GradleException("Cannot find 'nightly-url.txt' for nightly build! It must exist in the root of this project and contain the location of the nightly manifest.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadKeystoreProperties(filename: String): Properties? {
|
||||||
|
val keystorePropertiesFile = file("${project.rootDir}/$filename")
|
||||||
|
|
||||||
|
return if (keystorePropertiesFile.exists()) {
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
|
keystoreProperties
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDateSuffix(): String {
|
||||||
|
return SimpleDateFormat("yyyy-MM-dd-HH:mm").format(Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMapsKey(): String {
|
||||||
|
val mapKey = file("${project.rootDir}/maps.key")
|
||||||
|
|
||||||
|
return if (mapKey.exists()) {
|
||||||
|
mapKey.readLines()[0]
|
||||||
|
} else {
|
||||||
|
"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Project.languageList(): List<String> {
|
||||||
|
return fileTree("src/main/res") { include("**/strings.xml") }
|
||||||
|
.map { stringFile -> stringFile.parentFile.name }
|
||||||
|
.map { valuesFolderName -> valuesFolderName.replace("values-", "") }
|
||||||
|
.filter { valuesFolderName -> valuesFolderName != "values" }
|
||||||
|
.map { languageCode -> languageCode.replace("-r", "_") }
|
||||||
|
.distinct()
|
||||||
|
.sorted() + "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.capitalize(): String {
|
||||||
|
return this.replaceFirstChar { it.uppercase() }
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
-keep class org.whispersystems.** { *; }
|
-keep class org.whispersystems.** { *; }
|
||||||
|
-keep class org.signal.libsignal.net.** { *; }
|
||||||
-keep class org.signal.libsignal.protocol.** { *; }
|
-keep class org.signal.libsignal.protocol.** { *; }
|
||||||
|
-keep class org.signal.libsignal.usernames.** { *; }
|
||||||
-keep class org.thoughtcrime.securesms.** { *; }
|
-keep class org.thoughtcrime.securesms.** { *; }
|
||||||
|
-keep class org.signal.donations.json.** { *; }
|
||||||
-keepclassmembers class ** {
|
-keepclassmembers class ** {
|
||||||
public void onEvent*(**);
|
public void onEvent*(**);
|
||||||
}
|
}
|
||||||
@@ -13,6 +16,10 @@
|
|||||||
|
|
||||||
-keep class androidx.window.** { *; }
|
-keep class androidx.window.** { *; }
|
||||||
|
|
||||||
|
-keepclassmembers class * extends androidx.constraintlayout.motion.widget.Key {
|
||||||
|
public <init>();
|
||||||
|
}
|
||||||
|
|
||||||
# AGP generated dont warns
|
# AGP generated dont warns
|
||||||
-dontwarn com.android.org.conscrypt.SSLParametersImpl
|
-dontwarn com.android.org.conscrypt.SSLParametersImpl
|
||||||
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
|
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.signal.core.util.logging.AndroidLogger
|
|||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
|
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
|
||||||
import org.thoughtcrime.securesms.database.LogDatabase
|
import org.thoughtcrime.securesms.database.LogDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
|
||||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
||||||
@@ -21,14 +21,12 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
|
|||||||
|
|
||||||
override fun initializeAppDependencies() {
|
override fun initializeAppDependencies() {
|
||||||
val default = ApplicationDependencyProvider(this)
|
val default = ApplicationDependencyProvider(this)
|
||||||
ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
|
AppDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
|
||||||
ApplicationDependencies.getDeadlockDetector().start()
|
AppDependencies.deadlockDetector.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initializeLogging() {
|
override fun initializeLogging() {
|
||||||
persistentLogger = PersistentLogger(this)
|
Log.initialize({ true }, AndroidLogger(), PersistentLogger(this), inMemoryLogger)
|
||||||
|
|
||||||
Log.initialize({ true }, AndroidLogger(), persistentLogger, inMemoryLogger)
|
|
||||||
|
|
||||||
SignalProtocolLoggerProvider.setProvider(CustomSignalProtocolLogger())
|
SignalProtocolLoggerProvider.setProvider(CustomSignalProtocolLogger())
|
||||||
|
|
||||||
@@ -37,4 +35,18 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
|
|||||||
LogDatabase.getInstance(this).logs.trimToSize()
|
LogDatabase.getInstance(this).logs.trimToSize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun beginJobLoop() = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some of the jobs can interfere with some of the instrumentation tests.
|
||||||
|
*
|
||||||
|
* For example, we may try to create a release channel recipient while doing
|
||||||
|
* an import/backup test.
|
||||||
|
*
|
||||||
|
* This can be used to start the job loop if needed for tests that rely on it.
|
||||||
|
*/
|
||||||
|
fun beginJobLoopForTests() {
|
||||||
|
super.beginJobLoop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,684 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.signal.core.util.Hex
|
||||||
|
import org.signal.core.util.SqlUtil
|
||||||
|
import org.signal.core.util.insertInto
|
||||||
|
import org.signal.core.util.readToList
|
||||||
|
import org.signal.core.util.readToSingleObject
|
||||||
|
import org.signal.core.util.requireBlob
|
||||||
|
import org.signal.core.util.requireLong
|
||||||
|
import org.signal.core.util.requireString
|
||||||
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.toInt
|
||||||
|
import org.signal.core.util.withinTransaction
|
||||||
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||||
|
import org.thoughtcrime.securesms.database.CallTable
|
||||||
|
import org.thoughtcrime.securesms.database.EmojiSearchTable
|
||||||
|
import org.thoughtcrime.securesms.database.MessageTable
|
||||||
|
import org.thoughtcrime.securesms.database.MessageTypes
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||||
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.util.Currency
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
typealias DatabaseData = Map<String, List<Map<String, Any?>>>
|
||||||
|
|
||||||
|
class BackupTest {
|
||||||
|
companion object {
|
||||||
|
val SELF_ACI = ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
|
||||||
|
val SELF_PNI = PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
|
||||||
|
const val SELF_E164 = "+10000000000"
|
||||||
|
val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32))
|
||||||
|
|
||||||
|
val ALICE_ACI = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
|
||||||
|
val ALICE_PNI = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
|
||||||
|
val ALICE_E164 = "+12222222222"
|
||||||
|
|
||||||
|
/** Columns that we don't need to check equality of */
|
||||||
|
private val IGNORED_COLUMNS: Map<String, Set<String>> = mapOf(
|
||||||
|
RecipientTable.TABLE_NAME to setOf(RecipientTable.STORAGE_SERVICE_ID),
|
||||||
|
MessageTable.TABLE_NAME to setOf(MessageTable.FROM_DEVICE_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Tables we don't need to check equality of */
|
||||||
|
private val IGNORED_TABLES: Set<String> = setOf(
|
||||||
|
EmojiSearchTable.TABLE_NAME,
|
||||||
|
"sqlite_sequence",
|
||||||
|
"message_fts_data",
|
||||||
|
"message_fts_idx",
|
||||||
|
"message_fts_docsize"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
SignalStore.account.setE164(SELF_E164)
|
||||||
|
SignalStore.account.setAci(SELF_ACI)
|
||||||
|
SignalStore.account.setPni(SELF_PNI)
|
||||||
|
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||||
|
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Will likely be removed soon")
|
||||||
|
@Test
|
||||||
|
fun emptyDatabase() {
|
||||||
|
backupTest { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Will likely be removed soon")
|
||||||
|
@Test
|
||||||
|
fun noteToSelf() {
|
||||||
|
backupTest {
|
||||||
|
individualChat(aci = SELF_ACI, givenName = "Note to Self") {
|
||||||
|
standardMessage(outgoing = true, body = "A")
|
||||||
|
standardMessage(outgoing = true, body = "B")
|
||||||
|
standardMessage(outgoing = true, body = "C")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Will likely be removed soon")
|
||||||
|
@Test
|
||||||
|
fun individualChat() {
|
||||||
|
backupTest {
|
||||||
|
individualChat(aci = ALICE_ACI, givenName = "Alice") {
|
||||||
|
val m1 = standardMessage(outgoing = true, body = "Outgoing 1")
|
||||||
|
val m2 = standardMessage(outgoing = false, body = "Incoming 1", read = true)
|
||||||
|
standardMessage(outgoing = true, body = "Outgoing 2", quotes = m2)
|
||||||
|
standardMessage(outgoing = false, body = "Incoming 2", quotes = m1, quoteTargetMissing = true, read = false)
|
||||||
|
standardMessage(outgoing = true, body = "Outgoing 3, with mention", randomMention = true)
|
||||||
|
standardMessage(outgoing = false, body = "Incoming 3, with style", read = false, randomStyling = true)
|
||||||
|
remoteDeletedMessage(outgoing = true)
|
||||||
|
remoteDeletedMessage(outgoing = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Will likely be removed soon")
|
||||||
|
@Test
|
||||||
|
fun individualRecipients() {
|
||||||
|
backupTest {
|
||||||
|
// Comprehensive example
|
||||||
|
individualRecipient(
|
||||||
|
aci = ALICE_ACI,
|
||||||
|
pni = ALICE_PNI,
|
||||||
|
e164 = ALICE_E164,
|
||||||
|
givenName = "Alice",
|
||||||
|
familyName = "Smith",
|
||||||
|
username = "alice.99",
|
||||||
|
hidden = false,
|
||||||
|
registeredState = RecipientTable.RegisteredState.REGISTERED,
|
||||||
|
profileKey = ProfileKey(Random.nextBytes(32)),
|
||||||
|
profileSharing = true,
|
||||||
|
hideStory = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Trying to get coverage of all the various values
|
||||||
|
individualRecipient(aci = ACI.from(UUID.randomUUID()), registeredState = RecipientTable.RegisteredState.NOT_REGISTERED)
|
||||||
|
individualRecipient(aci = ACI.from(UUID.randomUUID()), registeredState = RecipientTable.RegisteredState.UNKNOWN)
|
||||||
|
individualRecipient(pni = PNI.from(UUID.randomUUID()))
|
||||||
|
individualRecipient(e164 = "+15551234567")
|
||||||
|
individualRecipient(aci = ACI.from(UUID.randomUUID()), givenName = "Bob")
|
||||||
|
individualRecipient(aci = ACI.from(UUID.randomUUID()), familyName = "Smith")
|
||||||
|
individualRecipient(aci = ACI.from(UUID.randomUUID()), profileSharing = false)
|
||||||
|
individualRecipient(aci = ACI.from(UUID.randomUUID()), hideStory = true)
|
||||||
|
individualRecipient(aci = ACI.from(UUID.randomUUID()), hidden = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Will likely be removed soon")
|
||||||
|
@Test
|
||||||
|
fun individualCallLogs() {
|
||||||
|
backupTest {
|
||||||
|
val aliceId = individualRecipient(
|
||||||
|
aci = ALICE_ACI,
|
||||||
|
pni = ALICE_PNI,
|
||||||
|
e164 = ALICE_E164,
|
||||||
|
givenName = "Alice",
|
||||||
|
familyName = "Smith",
|
||||||
|
username = "alice.99",
|
||||||
|
hidden = false,
|
||||||
|
registeredState = RecipientTable.RegisteredState.REGISTERED,
|
||||||
|
profileKey = ProfileKey(Random.nextBytes(32)),
|
||||||
|
profileSharing = true,
|
||||||
|
hideStory = false
|
||||||
|
)
|
||||||
|
insertOneToOneCallVariations(1, 1, aliceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertOneToOneCallVariations(callId: Long, timestamp: Long, id: RecipientId): Long {
|
||||||
|
val directions = arrayOf(CallTable.Direction.INCOMING, CallTable.Direction.OUTGOING)
|
||||||
|
val callTypes = arrayOf(CallTable.Type.AUDIO_CALL, CallTable.Type.VIDEO_CALL)
|
||||||
|
val events = arrayOf(
|
||||||
|
CallTable.Event.MISSED,
|
||||||
|
CallTable.Event.OUTGOING_RING,
|
||||||
|
CallTable.Event.ONGOING,
|
||||||
|
CallTable.Event.ACCEPTED,
|
||||||
|
CallTable.Event.NOT_ACCEPTED
|
||||||
|
)
|
||||||
|
var callTimestamp: Long = timestamp
|
||||||
|
var currentCallId = callId
|
||||||
|
for (direction in directions) {
|
||||||
|
for (event in events) {
|
||||||
|
for (type in callTypes) {
|
||||||
|
insertOneToOneCall(callId = currentCallId, callTimestamp, id, type, direction, event)
|
||||||
|
callTimestamp++
|
||||||
|
currentCallId++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCallId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: CallTable.Type, direction: CallTable.Direction, event: CallTable.Event) {
|
||||||
|
val messageType: Long = CallTable.Call.getMessageType(type, direction, event)
|
||||||
|
|
||||||
|
SignalDatabase.rawDatabase.withinTransaction {
|
||||||
|
val recipient = Recipient.resolved(peer)
|
||||||
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||||
|
val outgoing = direction == CallTable.Direction.OUTGOING
|
||||||
|
|
||||||
|
val messageValues = contentValuesOf(
|
||||||
|
MessageTable.FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else peer.serialize(),
|
||||||
|
MessageTable.FROM_DEVICE_ID to 1,
|
||||||
|
MessageTable.TO_RECIPIENT_ID to if (outgoing) peer.serialize() else Recipient.self().id.serialize(),
|
||||||
|
MessageTable.DATE_RECEIVED to timestamp,
|
||||||
|
MessageTable.DATE_SENT to timestamp,
|
||||||
|
MessageTable.READ to 1,
|
||||||
|
MessageTable.TYPE to messageType,
|
||||||
|
MessageTable.THREAD_ID to threadId
|
||||||
|
)
|
||||||
|
|
||||||
|
val messageId = SignalDatabase.rawDatabase.insert(MessageTable.TABLE_NAME, null, messageValues)
|
||||||
|
|
||||||
|
val values = contentValuesOf(
|
||||||
|
CallTable.CALL_ID to callId,
|
||||||
|
CallTable.MESSAGE_ID to messageId,
|
||||||
|
CallTable.PEER to peer.serialize(),
|
||||||
|
CallTable.TYPE to CallTable.Type.serialize(type),
|
||||||
|
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
|
||||||
|
CallTable.EVENT to CallTable.Event.serialize(event),
|
||||||
|
CallTable.TIMESTAMP to timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.rawDatabase.insert(CallTable.TABLE_NAME, null, values)
|
||||||
|
|
||||||
|
SignalDatabase.threads.update(threadId, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Will likely be removed soon")
|
||||||
|
@Test
|
||||||
|
fun accountData() {
|
||||||
|
val context = AppDependencies.application
|
||||||
|
|
||||||
|
backupTest(validateKeyValue = true) {
|
||||||
|
val self = Recipient.self()
|
||||||
|
|
||||||
|
// TODO note-to-self archived
|
||||||
|
// TODO note-to-self unread
|
||||||
|
|
||||||
|
SignalStore.account.setAci(SELF_ACI)
|
||||||
|
SignalStore.account.setPni(SELF_PNI)
|
||||||
|
SignalStore.account.setE164(SELF_E164)
|
||||||
|
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||||
|
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||||
|
|
||||||
|
SignalDatabase.recipients.setProfileKey(self.id, ProfileKey(Random.nextBytes(32)))
|
||||||
|
SignalDatabase.recipients.setProfileName(self.id, ProfileName.fromParts("Peter", "Parker"))
|
||||||
|
SignalDatabase.recipients.setProfileAvatar(self.id, "https://example.com/")
|
||||||
|
|
||||||
|
InAppPaymentsRepository.setSubscriber(InAppPaymentSubscriberRecord(SubscriberId.generate(), Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION, false, InAppPaymentData.PaymentMethodType.UNKNOWN))
|
||||||
|
SignalStore.inAppPayments.setDisplayBadgesOnProfile(false)
|
||||||
|
|
||||||
|
SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
|
||||||
|
SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
|
||||||
|
|
||||||
|
SignalStore.settings.isLinkPreviewsEnabled = false
|
||||||
|
SignalStore.settings.isPreferSystemContactPhotos = true
|
||||||
|
SignalStore.settings.universalExpireTimer = 42
|
||||||
|
SignalStore.settings.setKeepMutedChatsArchived(true)
|
||||||
|
|
||||||
|
SignalStore.story.viewedReceiptsEnabled = false
|
||||||
|
SignalStore.story.userHasViewedOnboardingStory = true
|
||||||
|
SignalStore.story.isFeatureDisabled = false
|
||||||
|
SignalStore.story.userHasBeenNotifiedAboutStories = true
|
||||||
|
SignalStore.story.userHasSeenGroupStoryEducationSheet = true
|
||||||
|
|
||||||
|
SignalStore.emoji.reactions = listOf("a", "b", "c")
|
||||||
|
|
||||||
|
TextSecurePreferences.setTypingIndicatorsEnabled(context, false)
|
||||||
|
TextSecurePreferences.setReadReceiptsEnabled(context, false)
|
||||||
|
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Have to check TextSecurePreferences ourselves, since they're not in a database
|
||||||
|
TextSecurePreferences.isTypingIndicatorsEnabled(context) assertIs false
|
||||||
|
TextSecurePreferences.isReadReceiptsEnabled(context) assertIs false
|
||||||
|
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context) assertIs true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the database, then executes your setup code, then compares snapshots of the database
|
||||||
|
* before an after an import to ensure that no data was lost/changed.
|
||||||
|
*
|
||||||
|
* @param validateKeyValue If true, this will also validate the KeyValueDatabase. You only want to do this if you
|
||||||
|
* intend on setting most of the values. Otherwise stuff tends to not match since values are lazily written.
|
||||||
|
*/
|
||||||
|
private fun backupTest(validateKeyValue: Boolean = false, content: () -> Unit) {
|
||||||
|
// Under normal circumstances, My Story ends up being the first recipient in the table, and is added automatically.
|
||||||
|
// This screws with the tests by offsetting all the recipientIds in the initial state.
|
||||||
|
// Easiest way to get around this is to make the DB a true clean slate by clearing everything.
|
||||||
|
// (We only really need to clear Recipient/dlists, but doing everything to be consistent.)
|
||||||
|
SignalDatabase.distributionLists.clearAllDataForBackupRestore()
|
||||||
|
SignalDatabase.recipients.clearAllDataForBackupRestore()
|
||||||
|
SignalDatabase.messages.clearAllDataForBackupRestore()
|
||||||
|
SignalDatabase.threads.clearAllDataForBackupRestore()
|
||||||
|
|
||||||
|
// Again, for comparison purposes, because we always import self first, we want to ensure it's the first item
|
||||||
|
// in the table when we export.
|
||||||
|
individualRecipient(
|
||||||
|
aci = SELF_ACI,
|
||||||
|
pni = SELF_PNI,
|
||||||
|
e164 = SELF_E164,
|
||||||
|
profileKey = SELF_PROFILE_KEY,
|
||||||
|
profileSharing = true
|
||||||
|
)
|
||||||
|
|
||||||
|
content()
|
||||||
|
|
||||||
|
val startingMainData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
|
||||||
|
val startingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
|
||||||
|
|
||||||
|
val exported: ByteArray = BackupRepository.export()
|
||||||
|
BackupRepository.import(length = exported.size.toLong(), inputStreamFactory = { ByteArrayInputStream(exported) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
|
||||||
|
|
||||||
|
val endingData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
|
||||||
|
val endingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
|
||||||
|
|
||||||
|
assertDatabaseMatches(startingMainData, endingData)
|
||||||
|
assertDatabaseMatches(startingKeyValueData, endingKeyValueData)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun individualChat(aci: ACI, givenName: String, familyName: String? = null, init: IndividualChatCreator.() -> Unit) {
|
||||||
|
val recipientId = individualRecipient(aci = aci, givenName = givenName, familyName = familyName, profileSharing = true)
|
||||||
|
|
||||||
|
val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
|
||||||
|
|
||||||
|
IndividualChatCreator(SignalDatabase.rawDatabase, recipientId, threadId).init()
|
||||||
|
|
||||||
|
SignalDatabase.threads.update(threadId, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun individualRecipient(
|
||||||
|
aci: ACI? = null,
|
||||||
|
pni: PNI? = null,
|
||||||
|
e164: String? = null,
|
||||||
|
givenName: String? = null,
|
||||||
|
familyName: String? = null,
|
||||||
|
username: String? = null,
|
||||||
|
hidden: Boolean = false,
|
||||||
|
registeredState: RecipientTable.RegisteredState = RecipientTable.RegisteredState.UNKNOWN,
|
||||||
|
profileKey: ProfileKey? = null,
|
||||||
|
profileSharing: Boolean = false,
|
||||||
|
hideStory: Boolean = false
|
||||||
|
): RecipientId {
|
||||||
|
check(aci != null || pni != null || e164 != null)
|
||||||
|
|
||||||
|
val recipientId = SignalDatabase.recipients.getAndPossiblyMerge(aci, pni, e164, pniVerified = true, changeSelf = true)
|
||||||
|
|
||||||
|
if (givenName != null || familyName != null) {
|
||||||
|
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts(givenName, familyName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username != null) {
|
||||||
|
SignalDatabase.recipients.setUsername(recipientId, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registeredState == RecipientTable.RegisteredState.REGISTERED) {
|
||||||
|
SignalDatabase.recipients.markRegistered(recipientId, aci ?: pni!!)
|
||||||
|
} else if (registeredState == RecipientTable.RegisteredState.NOT_REGISTERED) {
|
||||||
|
SignalDatabase.recipients.markUnregistered(recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileKey != null) {
|
||||||
|
SignalDatabase.recipients.setProfileKey(recipientId, profileKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalDatabase.recipients.setProfileSharing(recipientId, profileSharing)
|
||||||
|
SignalDatabase.recipients.setHideStory(recipientId, hideStory)
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
SignalDatabase.recipients.markHidden(recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipientId
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class IndividualChatCreator(
|
||||||
|
private val db: SQLiteDatabase,
|
||||||
|
private val recipientId: RecipientId,
|
||||||
|
private val threadId: Long
|
||||||
|
) {
|
||||||
|
fun standardMessage(
|
||||||
|
outgoing: Boolean,
|
||||||
|
sentTimestamp: Long = System.currentTimeMillis(),
|
||||||
|
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||||
|
serverTimestamp: Long = sentTimestamp,
|
||||||
|
body: String? = null,
|
||||||
|
read: Boolean = true,
|
||||||
|
quotes: Long? = null,
|
||||||
|
quoteTargetMissing: Boolean = false,
|
||||||
|
randomMention: Boolean = false,
|
||||||
|
randomStyling: Boolean = false
|
||||||
|
): Long {
|
||||||
|
return db.insertMessage(
|
||||||
|
from = if (outgoing) Recipient.self().id else recipientId,
|
||||||
|
to = if (outgoing) recipientId else Recipient.self().id,
|
||||||
|
outgoing = outgoing,
|
||||||
|
threadId = threadId,
|
||||||
|
sentTimestamp = sentTimestamp,
|
||||||
|
receivedTimestamp = receivedTimestamp,
|
||||||
|
serverTimestamp = serverTimestamp,
|
||||||
|
body = body,
|
||||||
|
read = read,
|
||||||
|
quotes = quotes,
|
||||||
|
quoteTargetMissing = quoteTargetMissing,
|
||||||
|
randomMention = randomMention,
|
||||||
|
randomStyling = randomStyling
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remoteDeletedMessage(
|
||||||
|
outgoing: Boolean,
|
||||||
|
sentTimestamp: Long = System.currentTimeMillis(),
|
||||||
|
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||||
|
serverTimestamp: Long = sentTimestamp
|
||||||
|
): Long {
|
||||||
|
return db.insertMessage(
|
||||||
|
from = if (outgoing) Recipient.self().id else recipientId,
|
||||||
|
to = if (outgoing) recipientId else Recipient.self().id,
|
||||||
|
outgoing = outgoing,
|
||||||
|
threadId = threadId,
|
||||||
|
sentTimestamp = sentTimestamp,
|
||||||
|
receivedTimestamp = receivedTimestamp,
|
||||||
|
serverTimestamp = serverTimestamp,
|
||||||
|
remoteDeleted = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SQLiteDatabase.insertMessage(
|
||||||
|
from: RecipientId,
|
||||||
|
to: RecipientId,
|
||||||
|
outgoing: Boolean,
|
||||||
|
threadId: Long,
|
||||||
|
sentTimestamp: Long = System.currentTimeMillis(),
|
||||||
|
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||||
|
serverTimestamp: Long = sentTimestamp,
|
||||||
|
body: String? = null,
|
||||||
|
read: Boolean = true,
|
||||||
|
quotes: Long? = null,
|
||||||
|
quoteTargetMissing: Boolean = false,
|
||||||
|
randomMention: Boolean = false,
|
||||||
|
randomStyling: Boolean = false,
|
||||||
|
remoteDeleted: Boolean = false
|
||||||
|
): Long {
|
||||||
|
val type = if (outgoing) {
|
||||||
|
MessageTypes.BASE_SENT_TYPE
|
||||||
|
} else {
|
||||||
|
MessageTypes.BASE_INBOX_TYPE
|
||||||
|
} or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT
|
||||||
|
|
||||||
|
val contentValues = ContentValues()
|
||||||
|
contentValues.put(MessageTable.DATE_SENT, sentTimestamp)
|
||||||
|
contentValues.put(MessageTable.DATE_RECEIVED, receivedTimestamp)
|
||||||
|
contentValues.put(MessageTable.FROM_RECIPIENT_ID, from.serialize())
|
||||||
|
contentValues.put(MessageTable.TO_RECIPIENT_ID, to.serialize())
|
||||||
|
contentValues.put(MessageTable.THREAD_ID, threadId)
|
||||||
|
contentValues.put(MessageTable.BODY, body)
|
||||||
|
contentValues.put(MessageTable.TYPE, type)
|
||||||
|
contentValues.put(MessageTable.READ, if (read) 1 else 0)
|
||||||
|
|
||||||
|
if (!outgoing) {
|
||||||
|
contentValues.put(MessageTable.DATE_SERVER, serverTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteDeleted) {
|
||||||
|
contentValues.put(MessageTable.REMOTE_DELETED, 1)
|
||||||
|
return this
|
||||||
|
.insertInto(MessageTable.TABLE_NAME)
|
||||||
|
.values(contentValues)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quotes != null) {
|
||||||
|
val quoteDetails = this.getQuoteDetailsFor(quotes)
|
||||||
|
contentValues.put(MessageTable.QUOTE_ID, if (quoteTargetMissing) MessageTable.QUOTE_TARGET_MISSING_ID else quoteDetails.quotedSentTimestamp)
|
||||||
|
contentValues.put(MessageTable.QUOTE_AUTHOR, quoteDetails.authorId.serialize())
|
||||||
|
contentValues.put(MessageTable.QUOTE_BODY, quoteDetails.body)
|
||||||
|
contentValues.put(MessageTable.QUOTE_BODY_RANGES, quoteDetails.bodyRanges)
|
||||||
|
contentValues.put(MessageTable.QUOTE_TYPE, quoteDetails.type)
|
||||||
|
contentValues.put(MessageTable.QUOTE_MISSING, quoteTargetMissing.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body != null && (randomMention || randomStyling)) {
|
||||||
|
val ranges: MutableList<BodyRangeList.BodyRange> = mutableListOf()
|
||||||
|
|
||||||
|
if (randomMention) {
|
||||||
|
ranges += BodyRangeList.BodyRange(
|
||||||
|
start = 0,
|
||||||
|
length = Random.nextInt(body.length),
|
||||||
|
mentionUuid = if (outgoing) Recipient.resolved(to).requireAci().toString() else Recipient.resolved(from).requireAci().toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (randomStyling) {
|
||||||
|
ranges += BodyRangeList.BodyRange(
|
||||||
|
start = 0,
|
||||||
|
length = Random.nextInt(body.length),
|
||||||
|
style = BodyRangeList.BodyRange.Style.fromValue(Random.nextInt(BodyRangeList.BodyRange.Style.values().size))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentValues.put(MessageTable.MESSAGE_RANGES, BodyRangeList(ranges = ranges).encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
.insertInto(MessageTable.TABLE_NAME)
|
||||||
|
.values(contentValues)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertDatabaseMatches(expected: DatabaseData, actual: DatabaseData) {
|
||||||
|
assert(expected.keys.size == actual.keys.size) { "Mismatched table count! Expected: ${expected.keys} || Actual: ${actual.keys}" }
|
||||||
|
assert(expected.keys.containsAll(actual.keys)) { "Table names differ! Expected: ${expected.keys} || Actual: ${actual.keys}" }
|
||||||
|
|
||||||
|
val tablesToCheck = expected.keys.filter { !IGNORED_TABLES.contains(it) }
|
||||||
|
|
||||||
|
for (table in tablesToCheck) {
|
||||||
|
val expectedTable: List<Map<String, Any?>> = expected[table]!!
|
||||||
|
val actualTable: List<Map<String, Any?>> = actual[table]!!
|
||||||
|
|
||||||
|
assert(expectedTable.size == actualTable.size) { "Mismatched number of rows for table '$table'! Expected: ${expectedTable.size} || Actual: ${actualTable.size}\n $actualTable" }
|
||||||
|
|
||||||
|
val expectedFiltered: List<Map<String, Any?>> = expectedTable.withoutExcludedColumns(IGNORED_COLUMNS[table])
|
||||||
|
val actualFiltered: List<Map<String, Any?>> = actualTable.withoutExcludedColumns(IGNORED_COLUMNS[table])
|
||||||
|
|
||||||
|
assert(contentEquals(expectedFiltered, actualFiltered)) { "Data did not match for table '$table'!\n${prettyDiff(expectedFiltered, actualFiltered)}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contentEquals(expectedRows: List<Map<String, Any?>>, actualRows: List<Map<String, Any?>>): Boolean {
|
||||||
|
if (expectedRows == actualRows) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(expectedRows.size == actualRows.size)
|
||||||
|
|
||||||
|
for (i in expectedRows.indices) {
|
||||||
|
val expectedRow = expectedRows[i]
|
||||||
|
val actualRow = actualRows[i]
|
||||||
|
|
||||||
|
for (key in expectedRow.keys) {
|
||||||
|
val expectedValue = expectedRow[key]
|
||||||
|
val actualValue = actualRow[key]
|
||||||
|
|
||||||
|
if (!contentEquals(expectedValue, actualValue)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contentEquals(lhs: Any?, rhs: Any?): Boolean {
|
||||||
|
return if (lhs is ByteArray && rhs is ByteArray) {
|
||||||
|
lhs.contentEquals(rhs)
|
||||||
|
} else {
|
||||||
|
lhs == rhs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prettyDiff(expectedRows: List<Map<String, Any?>>, actualRows: List<Map<String, Any?>>): String {
|
||||||
|
val builder = StringBuilder()
|
||||||
|
|
||||||
|
assert(expectedRows.size == actualRows.size)
|
||||||
|
|
||||||
|
for (i in expectedRows.indices) {
|
||||||
|
val expectedRow = expectedRows[i]
|
||||||
|
val actualRow = actualRows[i]
|
||||||
|
var describedRow = false
|
||||||
|
|
||||||
|
for (key in expectedRow.keys) {
|
||||||
|
val expectedValue = expectedRow[key]
|
||||||
|
val actualValue = actualRow[key]
|
||||||
|
|
||||||
|
if (!contentEquals(expectedValue, actualValue)) {
|
||||||
|
if (!describedRow) {
|
||||||
|
builder.append("-- ROW ${i + 1}\n")
|
||||||
|
describedRow = true
|
||||||
|
}
|
||||||
|
builder.append(" [$key] Expected: ${expectedValue.prettyPrint()} || Actual: ${actualValue.prettyPrint()} \n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (describedRow) {
|
||||||
|
builder.append("\n")
|
||||||
|
builder.append("Expected: $expectedRow\n")
|
||||||
|
builder.append("Actual: $actualRow\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Any?.prettyPrint(): String {
|
||||||
|
return when (this) {
|
||||||
|
is ByteArray -> "Bytes(${Hex.toString(this)})"
|
||||||
|
else -> this.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Map<String, Any?>>.withoutExcludedColumns(ignored: Set<String>?): List<Map<String, Any?>> {
|
||||||
|
return if (ignored != null) {
|
||||||
|
this.map { row ->
|
||||||
|
row.filterKeys { !ignored.contains(it) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SQLiteDatabase.getQuoteDetailsFor(messageId: Long): QuoteDetails {
|
||||||
|
return this
|
||||||
|
.select(
|
||||||
|
MessageTable.DATE_SENT,
|
||||||
|
MessageTable.FROM_RECIPIENT_ID,
|
||||||
|
MessageTable.BODY,
|
||||||
|
MessageTable.MESSAGE_RANGES
|
||||||
|
)
|
||||||
|
.from(MessageTable.TABLE_NAME)
|
||||||
|
.where("${MessageTable.ID} = ?", messageId)
|
||||||
|
.run()
|
||||||
|
.readToSingleObject { cursor ->
|
||||||
|
QuoteDetails(
|
||||||
|
quotedSentTimestamp = cursor.requireLong(MessageTable.DATE_SENT),
|
||||||
|
authorId = RecipientId.from(cursor.requireLong(MessageTable.FROM_RECIPIENT_ID)),
|
||||||
|
body = cursor.requireString(MessageTable.BODY),
|
||||||
|
bodyRanges = cursor.requireBlob(MessageTable.MESSAGE_RANGES),
|
||||||
|
type = QuoteModel.Type.NORMAL.code
|
||||||
|
)
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SQLiteDatabase.readAllContents(): DatabaseData {
|
||||||
|
return SqlUtil.getAllTables(this).associateWith { table -> this.getAllTableData(table) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SQLiteDatabase.getAllTableData(table: String): List<Map<String, Any?>> {
|
||||||
|
return this
|
||||||
|
.select()
|
||||||
|
.from(table)
|
||||||
|
.run()
|
||||||
|
.readToList { cursor ->
|
||||||
|
val map: MutableMap<String, Any?> = mutableMapOf()
|
||||||
|
|
||||||
|
for (i in 0 until cursor.columnCount) {
|
||||||
|
val column = cursor.getColumnName(i)
|
||||||
|
|
||||||
|
when (cursor.getType(i)) {
|
||||||
|
Cursor.FIELD_TYPE_INTEGER -> map[column] = cursor.getInt(i)
|
||||||
|
Cursor.FIELD_TYPE_FLOAT -> map[column] = cursor.getFloat(i)
|
||||||
|
Cursor.FIELD_TYPE_STRING -> map[column] = cursor.getString(i)
|
||||||
|
Cursor.FIELD_TYPE_BLOB -> map[column] = cursor.getBlob(i)
|
||||||
|
Cursor.FIELD_TYPE_NULL -> map[column] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class QuoteDetails(
|
||||||
|
val quotedSentTimestamp: Long,
|
||||||
|
val authorId: RecipientId,
|
||||||
|
val body: String?,
|
||||||
|
val bodyRanges: ByteArray?,
|
||||||
|
val type: Int
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
import org.signal.core.util.Base64
|
||||||
|
import org.signal.core.util.StreamUtil
|
||||||
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class ImportExportTestSuite(private val path: String) {
|
||||||
|
companion object {
|
||||||
|
val SELF_ACI = ServiceId.ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
|
||||||
|
val SELF_PNI = ServiceId.PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
|
||||||
|
const val SELF_E164 = "+10000000000"
|
||||||
|
val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32))
|
||||||
|
val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do")
|
||||||
|
|
||||||
|
const val TESTS_FOLDER = "backupTests"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Parameterized.Parameters(name = "{0}")
|
||||||
|
fun data(): Collection<Array<String>> {
|
||||||
|
val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)
|
||||||
|
return testFiles?.map { arrayOf(it) }!!.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234")
|
||||||
|
SignalStore.account.setE164(SELF_E164)
|
||||||
|
SignalStore.account.setAci(SELF_ACI)
|
||||||
|
SignalStore.account.setPni(SELF_PNI)
|
||||||
|
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||||
|
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBinProto() {
|
||||||
|
val binProtoBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("${TESTS_FOLDER}/$path").use {
|
||||||
|
StreamUtil.readFully(it)
|
||||||
|
}
|
||||||
|
import(binProtoBytes)
|
||||||
|
val generatedBackupData = BackupRepository.export()
|
||||||
|
compare(binProtoBytes, generatedBackupData)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun import(importData: ByteArray) {
|
||||||
|
BackupRepository.import(
|
||||||
|
length = importData.size.toLong(),
|
||||||
|
inputStreamFactory = { ByteArrayInputStream(importData) },
|
||||||
|
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY),
|
||||||
|
plaintext = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO compare with libsignal's library
|
||||||
|
private fun compare(import: ByteArray, export: ByteArray) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||||
|
import org.whispersystems.signalservice.api.util.toByteArray
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
object TestRecipientUtils {
|
||||||
|
|
||||||
|
private var upperGenAci = 13131313L
|
||||||
|
private var lowerGenAci = 0L
|
||||||
|
|
||||||
|
private var upperGenPni = 12121212L
|
||||||
|
private var lowerGenPni = 0L
|
||||||
|
|
||||||
|
private var groupMasterKeyRandom = Random(12345)
|
||||||
|
|
||||||
|
fun generateProfileKey(): ByteArray {
|
||||||
|
return ProfileKeyUtil.createNew().serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nextPni(): ByteArray {
|
||||||
|
synchronized(this) {
|
||||||
|
lowerGenPni++
|
||||||
|
var uuid = UUID(upperGenPni, lowerGenPni)
|
||||||
|
return uuid.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nextAci(): ByteArray {
|
||||||
|
synchronized(this) {
|
||||||
|
lowerGenAci++
|
||||||
|
var uuid = UUID(upperGenAci, lowerGenAci)
|
||||||
|
return uuid.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateGroupMasterKey(): ByteArray {
|
||||||
|
val masterKey = ByteArray(32)
|
||||||
|
groupMasterKeyRandom.nextBytes(masterKey)
|
||||||
|
return masterKey
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import androidx.test.filters.FlakyTest
|
|
||||||
import okhttp3.mockwebserver.MockResponse
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.signal.core.util.ThreadUtil
|
|
||||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
||||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
|
||||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
|
||||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
|
|
||||||
import org.thoughtcrime.securesms.testing.Get
|
|
||||||
import org.thoughtcrime.securesms.testing.MockProvider
|
|
||||||
import org.thoughtcrime.securesms.testing.Post
|
|
||||||
import org.thoughtcrime.securesms.testing.Put
|
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
|
||||||
import org.thoughtcrime.securesms.testing.assertIs
|
|
||||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
|
||||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
|
||||||
import org.thoughtcrime.securesms.testing.assertIsNull
|
|
||||||
import org.thoughtcrime.securesms.testing.assertIsSize
|
|
||||||
import org.thoughtcrime.securesms.testing.connectionFailure
|
|
||||||
import org.thoughtcrime.securesms.testing.failure
|
|
||||||
import org.thoughtcrime.securesms.testing.parsedRequestBody
|
|
||||||
import org.thoughtcrime.securesms.testing.success
|
|
||||||
import org.thoughtcrime.securesms.testing.timeout
|
|
||||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
|
||||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices
|
|
||||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ChangeNumberViewModelTest {
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val harness = SignalActivityRule()
|
|
||||||
|
|
||||||
private lateinit var viewModel: ChangeNumberViewModel
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
ThreadUtil.runOnMainSync {
|
|
||||||
viewModel = ChangeNumberViewModel(
|
|
||||||
localNumber = harness.self.requireE164(),
|
|
||||||
changeNumberRepository = ChangeNumberRepository(),
|
|
||||||
savedState = SavedStateHandle(),
|
|
||||||
password = SignalStore.account().servicePassword!!,
|
|
||||||
verifyAccountRepository = VerifyAccountRepository(harness.application)
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.setNewCountry(1)
|
|
||||||
viewModel.setNewNationalNumber("5555550102")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
|
|
||||||
// GIVEN
|
|
||||||
val aci = Recipient.self().requireServiceId()
|
|
||||||
val newPni = PNI.from(UUID.randomUUID())
|
|
||||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
|
||||||
lateinit var setPreKeysRequest: PreKeyState
|
|
||||||
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
|
||||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
|
||||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
|
||||||
Put("/v2/accounts/number") { r ->
|
|
||||||
changeNumberRequest = r.parsedRequestBody()
|
|
||||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
|
||||||
},
|
|
||||||
Put("/v2/keys") { r ->
|
|
||||||
setPreKeysRequest = r.parsedRequestBody()
|
|
||||||
MockResponse().success()
|
|
||||||
},
|
|
||||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
|
||||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If we encounter a server error, this means the server ack our request and rejected it. In this
|
|
||||||
* case we know the change *did not* take on the server and can reset to a clean state.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testChangeNumber_givenServerFailedApiCall() {
|
|
||||||
// GIVEN
|
|
||||||
val oldPni = Recipient.self().requirePni()
|
|
||||||
val oldE164 = Recipient.self().requireE164()
|
|
||||||
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
|
||||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
|
||||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
|
||||||
Put("/v2/accounts/number") { MockResponse().failure(500) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
|
||||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
processor.isServerSentError() assertIs true
|
|
||||||
Recipient.self().requireE164() assertIs oldE164
|
|
||||||
Recipient.self().requirePni() assertIs oldPni
|
|
||||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
|
|
||||||
* number on the server side. We have to do a whoami call to query the server for our details and then
|
|
||||||
* respond accordingly.
|
|
||||||
*
|
|
||||||
* In this case, the whoami is our old details, so we can know the change *did not* take on the server
|
|
||||||
* and can reset to a clean state.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() {
|
|
||||||
// GIVEN
|
|
||||||
val aci = Recipient.self().requireServiceId()
|
|
||||||
val oldPni = Recipient.self().requirePni()
|
|
||||||
val oldE164 = Recipient.self().requireE164()
|
|
||||||
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
|
||||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
|
||||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
|
||||||
Put("/v2/accounts/number") { MockResponse().connectionFailure() },
|
|
||||||
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
|
||||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
processor.isServerSentError() assertIs false
|
|
||||||
Recipient.self().requireE164() assertIs oldE164
|
|
||||||
Recipient.self().requirePni() assertIs oldPni
|
|
||||||
SignalStore.misc().isChangeNumberLocked assertIs false
|
|
||||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
|
|
||||||
* number on the server side. We have to do a whoami call to query the server for our details and then
|
|
||||||
* respond accordingly.
|
|
||||||
*
|
|
||||||
* In this case, the whoami is our new details, so we can know the change *did* take on the server
|
|
||||||
* and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock
|
|
||||||
* and apply the pending state after confirming the change on the server.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
@FlakyTest
|
|
||||||
@Ignore("Test sometimes requires manual intervention to continue.")
|
|
||||||
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
|
|
||||||
// GIVEN
|
|
||||||
val aci = Recipient.self().requireServiceId()
|
|
||||||
val oldPni = Recipient.self().requirePni()
|
|
||||||
val oldE164 = Recipient.self().requireE164()
|
|
||||||
val newPni = PNI.from(UUID.randomUUID())
|
|
||||||
|
|
||||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
|
||||||
lateinit var setPreKeysRequest: PreKeyState
|
|
||||||
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
|
||||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
|
||||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
|
||||||
Put("/v2/accounts/number") { r ->
|
|
||||||
changeNumberRequest = r.parsedRequestBody()
|
|
||||||
MockResponse().timeout()
|
|
||||||
},
|
|
||||||
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) },
|
|
||||||
Put("/v2/keys") { r ->
|
|
||||||
setPreKeysRequest = r.parsedRequestBody()
|
|
||||||
MockResponse().success()
|
|
||||||
},
|
|
||||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
|
||||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
processor.isServerSentError() assertIs false
|
|
||||||
Recipient.self().requireE164() assertIs oldE164
|
|
||||||
Recipient.self().requirePni() assertIs oldPni
|
|
||||||
SignalStore.misc().isChangeNumberLocked assertIs true
|
|
||||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNotNull()
|
|
||||||
|
|
||||||
// WHEN AGAIN Processing lock
|
|
||||||
val scenario = harness.launchActivity<ChangeNumberLockActivity>()
|
|
||||||
scenario.onActivity {}
|
|
||||||
ThreadUtil.sleep(500)
|
|
||||||
|
|
||||||
// THEN AGAIN
|
|
||||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
|
|
||||||
// GIVEN
|
|
||||||
val aci = Recipient.self().requireServiceId()
|
|
||||||
val newPni = PNI.from(UUID.randomUUID())
|
|
||||||
|
|
||||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
|
||||||
lateinit var setPreKeysRequest: PreKeyState
|
|
||||||
|
|
||||||
MockProvider.mockGetRegistrationLockStringFlow()
|
|
||||||
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
|
||||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
|
||||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
|
||||||
Put("/v2/accounts/number") { r ->
|
|
||||||
changeNumberRequest = r.parsedRequestBody()
|
|
||||||
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
|
|
||||||
MockResponse().failure(423, MockProvider.lockedFailure)
|
|
||||||
} else {
|
|
||||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Put("/v2/keys") { r ->
|
|
||||||
setPreKeysRequest = r.parsedRequestBody()
|
|
||||||
MockResponse().success()
|
|
||||||
},
|
|
||||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
|
||||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
|
|
||||||
processor.registrationLock() assertIs true
|
|
||||||
Recipient.self().requirePni() assertIsNot newPni
|
|
||||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
|
|
||||||
// GIVEN
|
|
||||||
val aci = Recipient.self().requireServiceId()
|
|
||||||
val newPni = PNI.from(UUID.randomUUID())
|
|
||||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
|
||||||
lateinit var setPreKeysRequest: PreKeyState
|
|
||||||
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
|
||||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
|
||||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
|
||||||
Put("/v2/accounts/number") { r ->
|
|
||||||
changeNumberRequest = r.parsedRequestBody()
|
|
||||||
if (changeNumberRequest.deviceMessages.isEmpty()) {
|
|
||||||
MockResponse().failure(
|
|
||||||
409,
|
|
||||||
MismatchedDevices().apply {
|
|
||||||
missingDevices = listOf(2)
|
|
||||||
extraDevices = emptyList()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Get("/v2/keys/$aci/2") {
|
|
||||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
|
|
||||||
},
|
|
||||||
Put("/v2/keys") { r ->
|
|
||||||
setPreKeysRequest = r.parsedRequestBody()
|
|
||||||
MockResponse().success()
|
|
||||||
},
|
|
||||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
|
||||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
|
|
||||||
// GIVEN
|
|
||||||
val aci = Recipient.self().requireServiceId()
|
|
||||||
val newPni = PNI.from(UUID.randomUUID())
|
|
||||||
|
|
||||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
|
||||||
lateinit var setPreKeysRequest: PreKeyState
|
|
||||||
|
|
||||||
MockProvider.mockGetRegistrationLockStringFlow()
|
|
||||||
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
|
||||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
|
||||||
Put("/v2/accounts/number") { r ->
|
|
||||||
changeNumberRequest = r.parsedRequestBody()
|
|
||||||
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
|
|
||||||
MockResponse().failure(423, MockProvider.lockedFailure)
|
|
||||||
} else if (changeNumberRequest.deviceMessages.isEmpty()) {
|
|
||||||
MockResponse().failure(
|
|
||||||
409,
|
|
||||||
MismatchedDevices().apply {
|
|
||||||
missingDevices = listOf(2)
|
|
||||||
extraDevices = emptyList()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (changeNumberRequest.deviceMessages.size == 1) {
|
|
||||||
MockResponse().failure(
|
|
||||||
409,
|
|
||||||
MismatchedDevices().apply {
|
|
||||||
missingDevices = listOf(2, 3)
|
|
||||||
extraDevices = emptyList()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Get("/v2/keys/$aci/2") {
|
|
||||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
|
|
||||||
},
|
|
||||||
Get("/v2/keys/$aci/3") {
|
|
||||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3))
|
|
||||||
},
|
|
||||||
Put("/v2/keys") { r ->
|
|
||||||
setPreKeysRequest = r.parsedRequestBody()
|
|
||||||
MockResponse().success()
|
|
||||||
},
|
|
||||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
|
||||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
|
|
||||||
processor.registrationLock() assertIs true
|
|
||||||
Recipient.self().requirePni() assertIsNot newPni
|
|
||||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
|
|
||||||
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
|
||||||
val pniMetadataStore = SignalStore.account().pniPreKeys
|
|
||||||
|
|
||||||
Recipient.self().requireE164() assertIs "+15555550102"
|
|
||||||
Recipient.self().requirePni() assertIs newPni
|
|
||||||
|
|
||||||
SignalStore.account().pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!!
|
|
||||||
SignalStore.account().pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey
|
|
||||||
pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId
|
|
||||||
|
|
||||||
val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId)
|
|
||||||
activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey
|
|
||||||
activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature
|
|
||||||
|
|
||||||
setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey
|
|
||||||
setPreKeysRequest.preKeys assertIsSize 100
|
|
||||||
|
|
||||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,14 +7,15 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.signal.core.util.ThreadUtil
|
import org.signal.core.util.ThreadUtil
|
||||||
|
import org.thoughtcrime.securesms.attachments.Cdn
|
||||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||||
|
import org.thoughtcrime.securesms.database.MessageType
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||||
@@ -64,7 +65,8 @@ class ConversationItemPreviewer {
|
|||||||
attachment()
|
attachment()
|
||||||
}
|
}
|
||||||
|
|
||||||
val message = IncomingMediaMessage(
|
val message = IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = other.id,
|
from = other.id,
|
||||||
body = body,
|
body = body,
|
||||||
sentTimeMillis = System.currentTimeMillis(),
|
sentTimeMillis = System.currentTimeMillis(),
|
||||||
@@ -73,7 +75,7 @@ class ConversationItemPreviewer {
|
|||||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||||
)
|
)
|
||||||
|
|
||||||
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
||||||
|
|
||||||
ThreadUtil.sleep(1)
|
ThreadUtil.sleep(1)
|
||||||
}
|
}
|
||||||
@@ -83,7 +85,8 @@ class ConversationItemPreviewer {
|
|||||||
attachment()
|
attachment()
|
||||||
}
|
}
|
||||||
|
|
||||||
val message = IncomingMediaMessage(
|
val message = IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = other.id,
|
from = other.id,
|
||||||
body = body,
|
body = body,
|
||||||
sentTimeMillis = System.currentTimeMillis(),
|
sentTimeMillis = System.currentTimeMillis(),
|
||||||
@@ -92,7 +95,7 @@ class ConversationItemPreviewer {
|
|||||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||||
)
|
)
|
||||||
|
|
||||||
val insert = SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
val insert = SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
||||||
|
|
||||||
SignalDatabase.attachments.getAttachmentsForMessage(insert.messageId).forEachIndexed { index, attachment ->
|
SignalDatabase.attachments.getAttachmentsForMessage(insert.messageId).forEachIndexed { index, attachment ->
|
||||||
// if (index != 1) {
|
// if (index != 1) {
|
||||||
@@ -134,7 +137,7 @@ class ConversationItemPreviewer {
|
|||||||
|
|
||||||
private fun attachment(): SignalServiceAttachmentPointer {
|
private fun attachment(): SignalServiceAttachmentPointer {
|
||||||
return SignalServiceAttachmentPointer(
|
return SignalServiceAttachmentPointer(
|
||||||
ReleaseChannel.CDN_NUMBER,
|
Cdn.CDN_3.cdnNumber,
|
||||||
SignalServiceAttachmentRemoteId.from(""),
|
SignalServiceAttachmentRemoteId.from(""),
|
||||||
"image/webp",
|
"image/webp",
|
||||||
null,
|
null,
|
||||||
@@ -144,13 +147,15 @@ class ConversationItemPreviewer {
|
|||||||
1024,
|
1024,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
|
0,
|
||||||
Optional.of("/not-there.jpg"),
|
Optional.of("/not-there.jpg"),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis(),
|
||||||
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.database.IdentityTable
|
|||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||||
@@ -64,7 +64,7 @@ class SafetyNumberChangeDialogPreviewer {
|
|||||||
scenario.onActivity { conversationActivity ->
|
scenario.onActivity { conversationActivity ->
|
||||||
SafetyNumberBottomSheet
|
SafetyNumberBottomSheet
|
||||||
.forIdentityRecordsAndDestinations(
|
.forIdentityRecordsAndDestinations(
|
||||||
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
|
identityRecords = AppDependencies.protocolStore.aci().identities().getIdentityRecords(othersRecipients).identityRecords,
|
||||||
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
|
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
|
||||||
)
|
)
|
||||||
.show(conversationActivity.supportFragmentManager)
|
.show(conversationActivity.supportFragmentManager)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ package org.thoughtcrime.securesms.conversation.v2.items
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import com.bumptech.glide.RequestManager
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@@ -29,7 +31,6 @@ import org.thoughtcrime.securesms.groups.GroupId
|
|||||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||||
@@ -47,7 +48,6 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||||
val actual = testSubject.setMessageShape(
|
val actual = testSubject.setMessageShape(
|
||||||
isLtr = true,
|
|
||||||
currentMessage = getMessageRecord(),
|
currentMessage = getMessageRecord(),
|
||||||
isGroupThread = false,
|
isGroupThread = false,
|
||||||
adapterPosition = 5
|
adapterPosition = 5
|
||||||
@@ -69,7 +69,6 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
val expected = V2ConversationItemShape.MessageShape.END
|
val expected = V2ConversationItemShape.MessageShape.END
|
||||||
val actual = testSubject.setMessageShape(
|
val actual = testSubject.setMessageShape(
|
||||||
isLtr = true,
|
|
||||||
currentMessage = getMessageRecord(now),
|
currentMessage = getMessageRecord(now),
|
||||||
isGroupThread = false,
|
isGroupThread = false,
|
||||||
adapterPosition = 5
|
adapterPosition = 5
|
||||||
@@ -91,7 +90,6 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
val expected = V2ConversationItemShape.MessageShape.START
|
val expected = V2ConversationItemShape.MessageShape.START
|
||||||
val actual = testSubject.setMessageShape(
|
val actual = testSubject.setMessageShape(
|
||||||
isLtr = true,
|
|
||||||
currentMessage = getMessageRecord(prev),
|
currentMessage = getMessageRecord(prev),
|
||||||
isGroupThread = false,
|
isGroupThread = false,
|
||||||
adapterPosition = 5
|
adapterPosition = 5
|
||||||
@@ -115,7 +113,6 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
val expected = V2ConversationItemShape.MessageShape.MIDDLE
|
val expected = V2ConversationItemShape.MessageShape.MIDDLE
|
||||||
val actual = testSubject.setMessageShape(
|
val actual = testSubject.setMessageShape(
|
||||||
isLtr = true,
|
|
||||||
currentMessage = getMessageRecord(now),
|
currentMessage = getMessageRecord(now),
|
||||||
isGroupThread = false,
|
isGroupThread = false,
|
||||||
adapterPosition = 5
|
adapterPosition = 5
|
||||||
@@ -137,7 +134,6 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||||
val actual = testSubject.setMessageShape(
|
val actual = testSubject.setMessageShape(
|
||||||
isLtr = true,
|
|
||||||
currentMessage = getMessageRecord(now),
|
currentMessage = getMessageRecord(now),
|
||||||
isGroupThread = false,
|
isGroupThread = false,
|
||||||
adapterPosition = 5
|
adapterPosition = 5
|
||||||
@@ -159,7 +155,6 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||||
val actual = testSubject.setMessageShape(
|
val actual = testSubject.setMessageShape(
|
||||||
isLtr = true,
|
|
||||||
currentMessage = getMessageRecord(prev),
|
currentMessage = getMessageRecord(prev),
|
||||||
isGroupThread = false,
|
isGroupThread = false,
|
||||||
adapterPosition = 5
|
adapterPosition = 5
|
||||||
@@ -183,7 +178,6 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||||
val actual = testSubject.setMessageShape(
|
val actual = testSubject.setMessageShape(
|
||||||
isLtr = true,
|
|
||||||
currentMessage = getMessageRecord(now),
|
currentMessage = getMessageRecord(now),
|
||||||
isGroupThread = false,
|
isGroupThread = false,
|
||||||
adapterPosition = 5
|
adapterPosition = 5
|
||||||
@@ -210,14 +204,15 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
private val colorizer = Colorizer()
|
private val colorizer = Colorizer()
|
||||||
|
|
||||||
|
override val lifecycleOwner: LifecycleOwner = mockk(relaxed = true)
|
||||||
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.Standard
|
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.Standard
|
||||||
|
|
||||||
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
|
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
|
||||||
override val selectedItems: Set<MultiselectPart> = emptySet()
|
override val selectedItems: Set<MultiselectPart> = emptySet()
|
||||||
override val isMessageRequestAccepted: Boolean = true
|
override val isMessageRequestAccepted: Boolean = true
|
||||||
override val searchQuery: String? = null
|
override val searchQuery: String? = null
|
||||||
override val glideRequests: GlideRequests = mockk()
|
override val requestManager: RequestManager = mockk()
|
||||||
override val isParentInScroll: Boolean = false
|
override val isParentInScroll: Boolean = false
|
||||||
|
override fun getChatColorsData(): ChatColorsDrawable.ChatColorsData = ChatColorsDrawable.ChatColorsData(null, null)
|
||||||
|
|
||||||
override fun onStartExpirationTimeout(messageRecord: MessageRecord) = Unit
|
override fun onStartExpirationTimeout(messageRecord: MessageRecord) = Unit
|
||||||
|
|
||||||
@@ -295,6 +290,8 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
override fun onChangeNumberUpdateContact(recipient: Recipient) = Unit
|
override fun onChangeNumberUpdateContact(recipient: Recipient) = Unit
|
||||||
|
|
||||||
|
override fun onChangeProfileNameUpdateContact(recipient: Recipient) = Unit
|
||||||
|
|
||||||
override fun onCallToAction(action: String) = Unit
|
override fun onCallToAction(action: String) = Unit
|
||||||
|
|
||||||
override fun onDonateClicked() = Unit
|
override fun onDonateClicked() = Unit
|
||||||
@@ -319,7 +316,7 @@ class V2ConversationItemShapeTest {
|
|||||||
|
|
||||||
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
|
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
|
||||||
|
|
||||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit
|
||||||
|
|
||||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
|
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
|
||||||
|
|
||||||
@@ -328,5 +325,14 @@ class V2ConversationItemShapeTest {
|
|||||||
override fun onItemClick(item: MultiselectPart?) = Unit
|
override fun onItemClick(item: MultiselectPart?) = Unit
|
||||||
|
|
||||||
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) = Unit
|
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) = Unit
|
||||||
|
|
||||||
|
override fun onShowSafetyTips(forGroup: Boolean) = Unit
|
||||||
|
|
||||||
|
override fun onReportSpamLearnMoreClicked() = Unit
|
||||||
|
|
||||||
|
override fun onMessageRequestAcceptOptionsClicked() = Unit
|
||||||
|
|
||||||
|
override fun onItemDoubleClick(item: MultiselectPart) = Unit
|
||||||
|
override fun onPaymentTombstoneClicked() = Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,18 +51,16 @@ class AttachmentTableTest {
|
|||||||
|
|
||||||
SignalDatabase.attachments.updateAttachmentData(
|
SignalDatabase.attachments.updateAttachmentData(
|
||||||
attachment,
|
attachment,
|
||||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
|
||||||
false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SignalDatabase.attachments.updateAttachmentData(
|
SignalDatabase.attachments.updateAttachmentData(
|
||||||
attachment2,
|
attachment2,
|
||||||
createMediaStream(byteArrayOf(1, 2, 3)),
|
createMediaStream(byteArrayOf(1, 2, 3))
|
||||||
false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
|
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
|
||||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
|
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
|
||||||
|
|
||||||
assertNotEquals(attachment1Info, attachment2Info)
|
assertNotEquals(attachment1Info, attachment2Info)
|
||||||
}
|
}
|
||||||
@@ -79,18 +77,16 @@ class AttachmentTableTest {
|
|||||||
|
|
||||||
SignalDatabase.attachments.updateAttachmentData(
|
SignalDatabase.attachments.updateAttachmentData(
|
||||||
attachment,
|
attachment,
|
||||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
|
||||||
true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SignalDatabase.attachments.updateAttachmentData(
|
SignalDatabase.attachments.updateAttachmentData(
|
||||||
attachment2,
|
attachment2,
|
||||||
createMediaStream(byteArrayOf(1, 2, 3, 4)),
|
createMediaStream(byteArrayOf(1, 2, 3, 4))
|
||||||
true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
|
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
|
||||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
|
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
|
||||||
|
|
||||||
assertNotEquals(attachment1Info, attachment2Info)
|
assertNotEquals(attachment1Info, attachment2Info)
|
||||||
}
|
}
|
||||||
@@ -121,15 +117,14 @@ class AttachmentTableTest {
|
|||||||
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
|
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false)
|
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData))
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA)!!
|
val previousInfo = SignalDatabase.attachments.getDataFileInfo(previousDatabaseAttachmentId)!!
|
||||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||||
|
|
||||||
assertNotEquals(standardInfo, highInfo)
|
assertNotEquals(standardInfo, highInfo)
|
||||||
standardInfo.file assertIs previousInfo.file
|
|
||||||
highInfo.file assertIsNot standardInfo.file
|
highInfo.file assertIsNot standardInfo.file
|
||||||
highInfo.file.exists() assertIs true
|
highInfo.file.exists() assertIs true
|
||||||
}
|
}
|
||||||
@@ -158,9 +153,9 @@ class AttachmentTableTest {
|
|||||||
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
|
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||||
val secondHighInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(secondHighDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
val secondHighInfo = SignalDatabase.attachments.getDataFileInfo(secondHighDatabaseAttachment.attachmentId)!!
|
||||||
|
|
||||||
highInfo.file assertIsNot standardInfo.file
|
highInfo.file assertIsNot standardInfo.file
|
||||||
secondHighInfo.file assertIs highInfo.file
|
secondHighInfo.file assertIs highInfo.file
|
||||||
|
|||||||
@@ -0,0 +1,838 @@
|
|||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.Base64
|
||||||
|
import org.signal.core.util.update
|
||||||
|
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||||
|
import org.thoughtcrime.securesms.attachments.Cdn
|
||||||
|
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.mms.MediaStream
|
||||||
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
|
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||||
|
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
import org.thoughtcrime.securesms.util.Util
|
||||||
|
import org.whispersystems.signalservice.api.backup.MediaId
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of [AttachmentTable] tests focused around deduping logic.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AttachmentTableTest_deduping {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DATA_A = byteArrayOf(1, 2, 3)
|
||||||
|
val DATA_A_COMPRESSED = byteArrayOf(4, 5, 6)
|
||||||
|
val DATA_A_HASH = byteArrayOf(1, 1, 1)
|
||||||
|
|
||||||
|
val DATA_B = byteArrayOf(7, 8, 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
SignalStore.account.setAci(ServiceId.ACI.from(UUID.randomUUID()))
|
||||||
|
SignalStore.account.setPni(ServiceId.PNI.from(UUID.randomUUID()))
|
||||||
|
SignalStore.account.setE164("+15558675309")
|
||||||
|
|
||||||
|
SignalDatabase.attachments.deleteAllAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates two different files with different data. Should not dedupe.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun differentFiles() {
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertWithData(DATA_B)
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts files with identical data but with transform properties that make them incompatible. Should not dedupe.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun identicalFiles_incompatibleTransforms() {
|
||||||
|
// Non-matching qualities
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||||
|
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-matching video trim flag
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties())
|
||||||
|
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true))
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-matching video trim start time
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||||
|
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-matching video trim end time
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 1))
|
||||||
|
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-matching mp4 fast start
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(mp4FastStart = true))
|
||||||
|
val id2 = insertWithData(DATA_A, TransformProperties(mp4FastStart = false))
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts files with identical data and compatible transform properties. Should dedupe.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun identicalFiles_compatibleTransforms() {
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, false)
|
||||||
|
assertSkipTransform(id2, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||||
|
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, false)
|
||||||
|
assertSkipTransform(id2, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||||
|
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, false)
|
||||||
|
assertSkipTransform(id2, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||||
|
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, false)
|
||||||
|
assertSkipTransform(id2, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks through various scenarios where files are compressed and uploaded.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun compressionAndUploads() {
|
||||||
|
// Matches after the first is compressed, skip transform properly set
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id2, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches after the first is uploaded, skip transform and ending hash properly set
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1)
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id2, true)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mimics sending two files at once. Ensures all fields are kept in sync as we compress and upload.
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, false)
|
||||||
|
assertSkipTransform(id2, false)
|
||||||
|
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id2, true)
|
||||||
|
|
||||||
|
upload(id1)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-use the upload when uploaded recently
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id2, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not re-use old uploads
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1, uploadTimestamp = System.currentTimeMillis() - 100.days.inWholeMilliseconds)
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id2, true)
|
||||||
|
|
||||||
|
assertDoesNotHaveRemoteFields(id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This isn't so much "desirable behavior" as it is documenting how things work.
|
||||||
|
// If an attachment is compressed but not uploaded yet, it will have a DATA_HASH_START that doesn't match the actual file content.
|
||||||
|
// This means that if we insert a new attachment with data that matches the compressed data, we won't find a match.
|
||||||
|
// This is ok because we don't allow forwarding unsent messages, so the chances of the user somehow sending a file that matches data we compressed are very low.
|
||||||
|
// What *is* more common is that the user may send DATA_A again, and in this case we will still catch the dedupe (which is already tested above).
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This represents what would happen if you forward an already-send compressed attachment. We should match, skip transform, and skip upload.
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This represents what would happen if you edited a video, sent it, then forwarded it. We should match, skip transform, and skip upload.
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This represents what would happen if you edited a video, sent it, then forwarded it, but *edited the forwarded video*. We should not dedupe.
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id2, false)
|
||||||
|
assertDoesNotHaveRemoteFields(id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This represents what would happen if you sent an image using standard quality, then forwarded it using high quality.
|
||||||
|
// Since you're forwarding, it doesn't matter if the new thing has a higher quality, we should still match and skip transform.
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This represents what would happen if you sent an image using high quality, then forwarded it using standard quality.
|
||||||
|
// Since you're forwarding, it doesn't matter if the new thing has a lower quality, we should still match and skip transform.
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertSkipTransform(id1, true)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that files marked as unhashable are all updated together
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
upload(id1)
|
||||||
|
upload(id2)
|
||||||
|
clearHashes(id1)
|
||||||
|
clearHashes(id2)
|
||||||
|
|
||||||
|
val file = dataFile(id1)
|
||||||
|
SignalDatabase.attachments.markDataFileAsUnhashable(file)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
|
||||||
|
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id1)!!
|
||||||
|
assertTrue(dataFileInfo.hashEnd!!.startsWith("UNHASHABLE-"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Various deletion scenarios to ensure that duped files don't deleted while there's still references.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun deletions() {
|
||||||
|
// Delete original then dupe
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
val dataFile = dataFile(id1)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
|
||||||
|
delete(id1)
|
||||||
|
|
||||||
|
assertDeleted(id1)
|
||||||
|
assertRowAndFileExists(id2)
|
||||||
|
assertTrue(dataFile.exists())
|
||||||
|
|
||||||
|
delete(id2)
|
||||||
|
|
||||||
|
assertDeleted(id2)
|
||||||
|
assertFalse(dataFile.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete dupe then original
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
val dataFile = dataFile(id1)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
|
||||||
|
delete(id2)
|
||||||
|
assertDeleted(id2)
|
||||||
|
assertRowAndFileExists(id1)
|
||||||
|
assertTrue(dataFile.exists())
|
||||||
|
|
||||||
|
delete(id1)
|
||||||
|
assertDeleted(id1)
|
||||||
|
assertFalse(dataFile.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete original after it was compressed
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
|
||||||
|
delete(id1)
|
||||||
|
|
||||||
|
assertDeleted(id1)
|
||||||
|
assertRowAndFileExists(id2)
|
||||||
|
assertSkipTransform(id2, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quotes are weak references and should not prevent us from deleting the file
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertQuote(id1)
|
||||||
|
|
||||||
|
val dataFile = dataFile(id1)
|
||||||
|
|
||||||
|
delete(id1)
|
||||||
|
assertDeleted(id1)
|
||||||
|
assertRowExists(id2)
|
||||||
|
assertFalse(dataFile.exists())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun quotes() {
|
||||||
|
// Basic quote deduping
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertQuote(id1)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Making sure remote fields carry
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertQuote(id1)
|
||||||
|
upload(id1)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashStartMatches(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Making sure things work for quotes of videos, which have trickier transform properties
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A, transformProperties = TransformProperties.forVideoTrim(1, 2))
|
||||||
|
compress(id1, DATA_A_COMPRESSED)
|
||||||
|
upload(id1)
|
||||||
|
|
||||||
|
val id2 = insertQuote(id1)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertRemoteFieldsMatch(id1, id2)
|
||||||
|
assertArchiveFieldsMatch(id1, id2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suite of tests around the migration where we hash all of the attachments and potentially dedupe them.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun migration() {
|
||||||
|
// Verifying that getUnhashedDataFile only returns if there's actually missing hashes
|
||||||
|
test {
|
||||||
|
val id = insertWithData(DATA_A)
|
||||||
|
upload(id)
|
||||||
|
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifying that getUnhashedDataFile finds the missing hash
|
||||||
|
test {
|
||||||
|
val id = insertWithData(DATA_A)
|
||||||
|
upload(id)
|
||||||
|
clearHashes(id)
|
||||||
|
assertNotNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifying that getUnhashedDataFile doesn't return if the file isn't done downloading
|
||||||
|
test {
|
||||||
|
val id = insertWithData(DATA_A)
|
||||||
|
upload(id)
|
||||||
|
setTransferState(id, AttachmentTable.TRANSFER_PROGRESS_PENDING)
|
||||||
|
clearHashes(id)
|
||||||
|
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||||
|
}
|
||||||
|
|
||||||
|
// If two attachments share the same file, when we backfill the hash, make sure both get their hashes set
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
upload(id1)
|
||||||
|
upload(id2)
|
||||||
|
|
||||||
|
clearHashes(id1)
|
||||||
|
clearHashes(id2)
|
||||||
|
|
||||||
|
val file = dataFile(id1)
|
||||||
|
SignalDatabase.attachments.setHashForDataFile(file, DATA_A_HASH)
|
||||||
|
|
||||||
|
assertDataHashEnd(id1, DATA_A_HASH)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a situation where two different attachments have the same data but wrote to different files, and verifies the migration dedupes it
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
upload(id1)
|
||||||
|
clearHashes(id1)
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
upload(id2)
|
||||||
|
clearHashes(id2)
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
|
||||||
|
val file1 = dataFile(id1)
|
||||||
|
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||||
|
|
||||||
|
assertDataHashEnd(id1, DATA_A_HASH)
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
|
||||||
|
val file2 = dataFile(id2)
|
||||||
|
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertFalse(file2.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've got three files now with the same data, with two of them sharing a file. We want to make sure *both* entries that share the same file get deduped.
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
upload(id1)
|
||||||
|
clearHashes(id1)
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
val id3 = insertWithData(DATA_A)
|
||||||
|
upload(id2)
|
||||||
|
upload(id3)
|
||||||
|
clearHashes(id2)
|
||||||
|
clearHashes(id3)
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
assertDataFilesAreTheSame(id2, id3)
|
||||||
|
|
||||||
|
val file1 = dataFile(id1)
|
||||||
|
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||||
|
assertDataHashEnd(id1, DATA_A_HASH)
|
||||||
|
|
||||||
|
val file2 = dataFile(id2)
|
||||||
|
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||||
|
|
||||||
|
assertDataFilesAreTheSame(id1, id2)
|
||||||
|
assertDataHashEndMatches(id1, id2)
|
||||||
|
assertDataHashEndMatches(id2, id3)
|
||||||
|
assertFalse(file2.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want to mess with files that are still downloading, so this makes sure that even if data matches, we don't dedupe and don't delete the file
|
||||||
|
test {
|
||||||
|
val id1 = insertWithData(DATA_A)
|
||||||
|
upload(id1)
|
||||||
|
clearHashes(id1)
|
||||||
|
|
||||||
|
val id2 = insertWithData(DATA_A)
|
||||||
|
// *not* uploaded
|
||||||
|
clearHashes(id2)
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
|
||||||
|
val file1 = dataFile(id1)
|
||||||
|
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||||
|
assertDataHashEnd(id1, DATA_A_HASH)
|
||||||
|
|
||||||
|
val file2 = dataFile(id2)
|
||||||
|
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||||
|
|
||||||
|
assertDataFilesAreDifferent(id1, id2)
|
||||||
|
assertTrue(file2.exists())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestContext {
|
||||||
|
fun insertWithData(data: ByteArray, transformProperties: TransformProperties = TransformProperties.empty()): AttachmentId {
|
||||||
|
val uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
||||||
|
|
||||||
|
val attachment = UriAttachmentBuilder.build(
|
||||||
|
id = Random.nextLong(),
|
||||||
|
uri = uri,
|
||||||
|
contentType = MediaUtil.IMAGE_JPEG,
|
||||||
|
transformProperties = transformProperties
|
||||||
|
)
|
||||||
|
|
||||||
|
return SignalDatabase.attachments.insertAttachmentForPreUpload(attachment).attachmentId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insertQuote(attachmentId: AttachmentId): AttachmentId {
|
||||||
|
val originalAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||||
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.self())
|
||||||
|
val messageId = SignalDatabase.messages.insertMessageOutbox(
|
||||||
|
message = OutgoingMessage(
|
||||||
|
threadRecipient = Recipient.self(),
|
||||||
|
sentTimeMillis = System.currentTimeMillis(),
|
||||||
|
body = "some text",
|
||||||
|
outgoingQuote = QuoteModel(
|
||||||
|
id = 123,
|
||||||
|
author = Recipient.self().id,
|
||||||
|
text = "Some quote text",
|
||||||
|
isOriginalMissing = false,
|
||||||
|
attachments = listOf(originalAttachment),
|
||||||
|
mentions = emptyList(),
|
||||||
|
type = QuoteModel.Type.NORMAL,
|
||||||
|
bodyRanges = null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
threadId = threadId,
|
||||||
|
forceSms = false,
|
||||||
|
insertListener = null
|
||||||
|
)
|
||||||
|
|
||||||
|
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
|
||||||
|
return attachments[0].attachmentId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun compress(attachmentId: AttachmentId, newData: ByteArray, mp4FastStart: Boolean = false) {
|
||||||
|
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||||
|
SignalDatabase.attachments.updateAttachmentData(databaseAttachment, newData.asMediaStream())
|
||||||
|
SignalDatabase.attachments.markAttachmentAsTransformed(attachmentId, withFastStart = mp4FastStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
|
||||||
|
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createPointerAttachment(attachmentId, uploadTimestamp), uploadTimestamp)
|
||||||
|
|
||||||
|
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||||
|
SignalDatabase.attachments.setArchiveData(
|
||||||
|
attachmentId = attachmentId,
|
||||||
|
archiveCdn = Cdn.CDN_3.cdnNumber,
|
||||||
|
archiveMediaName = attachment.getMediaName().name,
|
||||||
|
archiveThumbnailMediaId = MediaId(Util.getSecretBytes(15)).encode(),
|
||||||
|
archiveMediaId = MediaId(Util.getSecretBytes(15)).encode()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(attachmentId: AttachmentId) {
|
||||||
|
SignalDatabase.attachments.deleteAttachment(attachmentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dataFile(attachmentId: AttachmentId): File {
|
||||||
|
return SignalDatabase.attachments.getDataFileInfo(attachmentId)!!.file
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTransferState(attachmentId: AttachmentId, transferState: Int) {
|
||||||
|
// messageId doesn't actually matter -- that's for notifying listeners
|
||||||
|
SignalDatabase.attachments.setTransferState(messageId = -1, attachmentId = attachmentId, transferState = transferState)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearHashes(id: AttachmentId) {
|
||||||
|
SignalDatabase.attachments.writableDatabase
|
||||||
|
.update(AttachmentTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
AttachmentTable.DATA_HASH_START to null,
|
||||||
|
AttachmentTable.DATA_HASH_END to null
|
||||||
|
)
|
||||||
|
.where("${AttachmentTable.ID} = ?", id)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertDeleted(attachmentId: AttachmentId) {
|
||||||
|
assertNull("$attachmentId exists, but it shouldn't!", SignalDatabase.attachments.getAttachment(attachmentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertRowAndFileExists(attachmentId: AttachmentId) {
|
||||||
|
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||||
|
assertNotNull("$attachmentId does not exist!", databaseAttachment)
|
||||||
|
|
||||||
|
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(attachmentId)
|
||||||
|
assertTrue("The file for $attachmentId does not exist!", dataFileInfo!!.file.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertRowExists(attachmentId: AttachmentId) {
|
||||||
|
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||||
|
assertNotNull("$attachmentId does not exist!", databaseAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertDataFilesAreTheSame(lhs: AttachmentId, rhs: AttachmentId) {
|
||||||
|
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||||
|
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||||
|
|
||||||
|
assert(lhsInfo.file.exists())
|
||||||
|
assert(rhsInfo.file.exists())
|
||||||
|
|
||||||
|
assertEquals(lhsInfo.file, rhsInfo.file)
|
||||||
|
assertEquals(lhsInfo.length, rhsInfo.length)
|
||||||
|
assertArrayEquals(lhsInfo.random, rhsInfo.random)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertDataFilesAreDifferent(lhs: AttachmentId, rhs: AttachmentId) {
|
||||||
|
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||||
|
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||||
|
|
||||||
|
assert(lhsInfo.file.exists())
|
||||||
|
assert(rhsInfo.file.exists())
|
||||||
|
|
||||||
|
assertNotEquals(lhsInfo.file, rhsInfo.file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertDataHashStartMatches(lhs: AttachmentId, rhs: AttachmentId) {
|
||||||
|
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||||
|
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||||
|
|
||||||
|
assertNotNull(lhsInfo.hashStart)
|
||||||
|
assertEquals("DATA_HASH_START's did not match!", lhsInfo.hashStart, rhsInfo.hashStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertDataHashEndMatches(lhs: AttachmentId, rhs: AttachmentId) {
|
||||||
|
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||||
|
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||||
|
|
||||||
|
assertNotNull(lhsInfo.hashEnd)
|
||||||
|
assertEquals("DATA_HASH_END's did not match!", lhsInfo.hashEnd, rhsInfo.hashEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertDataHashEnd(id: AttachmentId, byteArray: ByteArray) {
|
||||||
|
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id)!!
|
||||||
|
assertArrayEquals(byteArray, Base64.decode(dataFileInfo.hashEnd!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertRemoteFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
|
||||||
|
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
|
||||||
|
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
|
||||||
|
|
||||||
|
assertEquals(lhsAttachment.remoteLocation, rhsAttachment.remoteLocation)
|
||||||
|
assertEquals(lhsAttachment.remoteKey, rhsAttachment.remoteKey)
|
||||||
|
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
|
||||||
|
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
|
||||||
|
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
|
||||||
|
assertEquals(lhsAttachment.cdn.cdnNumber, rhsAttachment.cdn.cdnNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertArchiveFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
|
||||||
|
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
|
||||||
|
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
|
||||||
|
|
||||||
|
assertEquals(lhsAttachment.archiveCdn, rhsAttachment.archiveCdn)
|
||||||
|
assertEquals(lhsAttachment.archiveMediaName, rhsAttachment.archiveMediaName)
|
||||||
|
assertEquals(lhsAttachment.archiveMediaId, rhsAttachment.archiveMediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
|
||||||
|
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||||
|
assertEquals(0, databaseAttachment.uploadTimestamp)
|
||||||
|
assertNull(databaseAttachment.remoteLocation)
|
||||||
|
assertNull(databaseAttachment.remoteDigest)
|
||||||
|
assertNull(databaseAttachment.remoteKey)
|
||||||
|
assertEquals(0, databaseAttachment.cdn.cdnNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
|
||||||
|
val transformProperties = SignalDatabase.attachments.getTransformProperties(attachmentId)!!
|
||||||
|
assertEquals("Incorrect skipTransform!", transformProperties.skipTransform, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.asMediaStream(): MediaStream {
|
||||||
|
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPointerAttachment(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): PointerAttachment {
|
||||||
|
val location = "somewhere-${Random.nextLong()}"
|
||||||
|
val key = "somekey-${Random.nextLong()}"
|
||||||
|
val digest = Random.nextBytes(32)
|
||||||
|
val incrementalDigest = Random.nextBytes(16)
|
||||||
|
|
||||||
|
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||||
|
|
||||||
|
return PointerAttachment(
|
||||||
|
"image/jpeg",
|
||||||
|
AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||||
|
databaseAttachment.size, // size
|
||||||
|
null,
|
||||||
|
Cdn.CDN_3, // cdnNumber
|
||||||
|
location,
|
||||||
|
key,
|
||||||
|
digest,
|
||||||
|
incrementalDigest,
|
||||||
|
5, // incrementalMacChunkSize
|
||||||
|
null,
|
||||||
|
databaseAttachment.voiceNote,
|
||||||
|
databaseAttachment.borderless,
|
||||||
|
databaseAttachment.videoGif,
|
||||||
|
databaseAttachment.width,
|
||||||
|
databaseAttachment.height,
|
||||||
|
uploadTimestamp,
|
||||||
|
databaseAttachment.caption,
|
||||||
|
databaseAttachment.stickerLocator,
|
||||||
|
databaseAttachment.blurHash,
|
||||||
|
databaseAttachment.uuid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun test(content: TestContext.() -> Unit) {
|
||||||
|
SignalDatabase.attachments.deleteAllAttachments()
|
||||||
|
val context = TestContext()
|
||||||
|
context.content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ class CallTableTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
SignalDatabase.calls.deleteGroupCall(call!!)
|
SignalDatabase.calls.markCallDeletedFromSyncEvent(call!!)
|
||||||
|
|
||||||
val deletedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
val deletedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
||||||
@@ -69,9 +69,10 @@ class CallTableTest {
|
|||||||
@Test
|
@Test
|
||||||
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
|
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
|
||||||
val callId = 1L
|
val callId = 1L
|
||||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
|
||||||
callId,
|
callId,
|
||||||
groupRecipientId,
|
groupRecipientId,
|
||||||
|
CallTable.Type.GROUP_CALL,
|
||||||
CallTable.Direction.OUTGOING,
|
CallTable.Direction.OUTGOING,
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
@@ -214,6 +215,175 @@ class CallTableTest {
|
|||||||
assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
|
assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAnOutgoingRingCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
|
||||||
|
val callId = 1L
|
||||||
|
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||||
|
callId = callId,
|
||||||
|
recipientId = groupRecipientId,
|
||||||
|
direction = CallTable.Direction.OUTGOING,
|
||||||
|
timestamp = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertNotNull(call)
|
||||||
|
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||||
|
call!!
|
||||||
|
)
|
||||||
|
|
||||||
|
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenARingingCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
|
||||||
|
val callId = 1L
|
||||||
|
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||||
|
ringId = callId,
|
||||||
|
groupRecipientId = groupRecipientId,
|
||||||
|
ringerRecipient = harness.others[1],
|
||||||
|
dateReceived = System.currentTimeMillis(),
|
||||||
|
ringState = CallManager.RingUpdate.REQUESTED
|
||||||
|
)
|
||||||
|
|
||||||
|
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertNotNull(call)
|
||||||
|
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||||
|
call!!
|
||||||
|
)
|
||||||
|
|
||||||
|
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAMissedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
|
||||||
|
val callId = 1L
|
||||||
|
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||||
|
ringId = callId,
|
||||||
|
groupRecipientId = groupRecipientId,
|
||||||
|
ringerRecipient = harness.others[1],
|
||||||
|
dateReceived = System.currentTimeMillis(),
|
||||||
|
ringState = CallManager.RingUpdate.EXPIRED_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertNotNull(call)
|
||||||
|
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||||
|
call!!
|
||||||
|
)
|
||||||
|
|
||||||
|
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenADeclinedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
|
||||||
|
val callId = 1L
|
||||||
|
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||||
|
ringId = callId,
|
||||||
|
groupRecipientId = groupRecipientId,
|
||||||
|
ringerRecipient = harness.others[1],
|
||||||
|
dateReceived = System.currentTimeMillis(),
|
||||||
|
ringState = CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||||
|
)
|
||||||
|
|
||||||
|
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertNotNull(call)
|
||||||
|
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||||
|
call!!
|
||||||
|
)
|
||||||
|
|
||||||
|
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAnAcceptedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
|
||||||
|
val callId = 1L
|
||||||
|
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||||
|
ringId = callId,
|
||||||
|
groupRecipientId = groupRecipientId,
|
||||||
|
ringerRecipient = harness.others[1],
|
||||||
|
dateReceived = System.currentTimeMillis(),
|
||||||
|
ringState = CallManager.RingUpdate.REQUESTED
|
||||||
|
)
|
||||||
|
|
||||||
|
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertNotNull(call)
|
||||||
|
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptIncomingGroupCall(
|
||||||
|
call!!
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||||
|
SignalDatabase.calls.getCallById(callId, groupRecipientId)!!
|
||||||
|
)
|
||||||
|
|
||||||
|
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGenericGroupCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
|
||||||
|
val era = "aaa"
|
||||||
|
val callId = CallId.fromEra(era).longValue()
|
||||||
|
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||||
|
groupRecipientId = groupRecipientId,
|
||||||
|
sender = harness.others[1],
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
peekGroupCallEraId = "aaa",
|
||||||
|
peekJoinedUuids = emptyList(),
|
||||||
|
isCallFull = false
|
||||||
|
)
|
||||||
|
|
||||||
|
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertNotNull(call)
|
||||||
|
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||||
|
call!!
|
||||||
|
)
|
||||||
|
|
||||||
|
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAJoinedGroupCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
|
||||||
|
val era = "aaa"
|
||||||
|
val callId = CallId.fromEra(era).longValue()
|
||||||
|
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||||
|
groupRecipientId = groupRecipientId,
|
||||||
|
sender = harness.others[1],
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
peekGroupCallEraId = "aaa",
|
||||||
|
peekJoinedUuids = emptyList(),
|
||||||
|
isCallFull = false
|
||||||
|
)
|
||||||
|
|
||||||
|
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertNotNull(call)
|
||||||
|
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptIncomingGroupCall(
|
||||||
|
call!!
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.calls.acceptOutgoingGroupCall(SignalDatabase.calls.getCallById(callId, groupRecipientId)!!)
|
||||||
|
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||||
|
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
|
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
|
||||||
val era = "aaa"
|
val era = "aaa"
|
||||||
@@ -269,11 +439,12 @@ class CallTableTest {
|
|||||||
@Test
|
@Test
|
||||||
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
|
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
|
||||||
val callId = 1L
|
val callId = 1L
|
||||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
recipientId = groupRecipientId,
|
recipientId = groupRecipientId,
|
||||||
direction = CallTable.Direction.INCOMING,
|
direction = CallTable.Direction.INCOMING,
|
||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis(),
|
||||||
|
type = CallTable.Type.GROUP_CALL
|
||||||
)
|
)
|
||||||
|
|
||||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import org.signal.core.util.getIndexes
|
|||||||
import org.signal.core.util.readToList
|
import org.signal.core.util.readToList
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
|
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +30,7 @@ class DatabaseConsistencyTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testUpgradeConsistency() {
|
fun testUpgradeConsistency() {
|
||||||
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
|
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
|
||||||
val testHelper = InMemoryTestHelper(ApplicationDependencies.getApplication()).also {
|
val testHelper = InMemoryTestHelper(AppDependencies.application).also {
|
||||||
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
|
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
@@ -26,7 +26,7 @@ class DatabaseObserverTest {
|
|||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
db = SignalDatabase.instance!!.signalWritableDatabase
|
db = SignalDatabase.instance!!.signalWritableDatabase
|
||||||
observer = ApplicationDependencies.getDatabaseObserver()
|
observer = AppDependencies.databaseObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -25,15 +25,6 @@ class DistributionListTablesTest {
|
|||||||
Assert.assertNotNull(id)
|
Assert.assertNotNull(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun createList_whenNameConflict_failToInsert() {
|
|
||||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
|
||||||
Assert.assertNotNull(id)
|
|
||||||
|
|
||||||
val id2: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
|
||||||
Assert.assertNull(id2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getList_returnCorrectList() {
|
fun getList_returnCorrectList() {
|
||||||
createRecipients(3)
|
createRecipients(3)
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNotEquals
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.signal.core.util.delete
|
import org.signal.core.util.deleteAll
|
||||||
import org.signal.core.util.readToList
|
import org.signal.core.util.readToList
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.withinTransaction
|
import org.signal.core.util.withinTransaction
|
||||||
@@ -33,8 +32,8 @@ class GroupTableTest {
|
|||||||
fun setUp() {
|
fun setUp() {
|
||||||
groupTable = SignalDatabase.groups
|
groupTable = SignalDatabase.groups
|
||||||
|
|
||||||
groupTable.writableDatabase.delete(GroupTable.TABLE_NAME).run()
|
groupTable.writableDatabase.deleteAll(GroupTable.TABLE_NAME)
|
||||||
groupTable.writableDatabase.delete(GroupTable.MembershipTable.TABLE_NAME).run()
|
groupTable.writableDatabase.deleteAll(GroupTable.MembershipTable.TABLE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -75,21 +74,6 @@ class GroupTableTest {
|
|||||||
assertEquals(2, groups.size)
|
assertEquals(2, groups.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
|
|
||||||
insertPushGroup()
|
|
||||||
insertMmsGroup(members = listOf(harness.others[1]))
|
|
||||||
|
|
||||||
val groups = groupTable.queryGroupsByMembership(
|
|
||||||
setOf(harness.self.id, harness.others[1]),
|
|
||||||
includeInactive = false,
|
|
||||||
excludeV1 = false,
|
|
||||||
excludeMms = false
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(2, groups.cursor?.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
|
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
|
||||||
insertPushGroup()
|
insertPushGroup()
|
||||||
@@ -181,72 +165,10 @@ class GroupTableTest {
|
|||||||
assertFalse(actual)
|
assertFalse(actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
|
|
||||||
val v2Group = insertPushGroup()
|
|
||||||
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
|
|
||||||
val groupRecord = groupTable.getGroup(v2Group)
|
|
||||||
|
|
||||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
|
||||||
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
|
|
||||||
val other = insertMmsGroup(members + listOf(harness.others[1]))
|
|
||||||
val mmsGroup = insertMmsGroup(members)
|
|
||||||
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
|
|
||||||
|
|
||||||
assertNotEquals(other, actual)
|
|
||||||
assertEquals(mmsGroup, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenMultipleMmsGroups_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
|
||||||
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1])
|
|
||||||
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2])
|
|
||||||
|
|
||||||
val group1: GroupId = insertMmsGroup(group1Members)
|
|
||||||
val group2: GroupId = insertMmsGroup(group2Members)
|
|
||||||
|
|
||||||
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.toSet())
|
|
||||||
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.toSet())
|
|
||||||
|
|
||||||
assertEquals(group1, group1Result)
|
|
||||||
assertEquals(group2, group2Result)
|
|
||||||
assertNotEquals(group1Result, group2Result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenMultipleMmsGroupsWithDifferentMemberOrders_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
|
||||||
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1], harness.others[2]).shuffled()
|
|
||||||
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2], harness.others[3]).shuffled()
|
|
||||||
|
|
||||||
val group1: GroupId = insertMmsGroup(group1Members)
|
|
||||||
val group2: GroupId = insertMmsGroup(group2Members)
|
|
||||||
|
|
||||||
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.shuffled().toSet())
|
|
||||||
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.shuffled().toSet())
|
|
||||||
|
|
||||||
assertEquals(group1, group1Result)
|
|
||||||
assertEquals(group2, group2Result)
|
|
||||||
assertNotEquals(group1Result, group2Result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenMmsGroupWithOneMember_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
|
||||||
val groupMembers: List<RecipientId> = listOf(harness.self.id)
|
|
||||||
val group: GroupId = insertMmsGroup(groupMembers)
|
|
||||||
|
|
||||||
val groupResult: GroupId = groupTable.getOrCreateMmsGroupForMembers(groupMembers.toSet())
|
|
||||||
|
|
||||||
assertEquals(group, groupResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
|
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
|
||||||
val g1 = insertPushGroup(listOf())
|
val g1 = insertPushGroup(members = emptyList())
|
||||||
val g2 = insertPushGroup(listOf())
|
val g2 = insertPushGroup(members = emptyList())
|
||||||
|
|
||||||
val gr1 = groupTable.getGroup(g1)
|
val gr1 = groupTable.getGroup(g1)
|
||||||
val gr2 = groupTable.getGroup(g2)
|
val gr2 = groupTable.getGroup(g2)
|
||||||
@@ -273,6 +195,85 @@ class GroupTableTest {
|
|||||||
assertEquals(groups[0].id, groupInCommon)
|
assertEquals(groups[0].id, groupInCommon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForTheSharedToken_thenIExpectBothGroups() {
|
||||||
|
insertPushGroup("Group Alice")
|
||||||
|
insertPushGroup("Group Bob")
|
||||||
|
|
||||||
|
SignalDatabase.groups.queryGroupsByTitle(
|
||||||
|
inputQuery = "Group",
|
||||||
|
includeInactive = false,
|
||||||
|
excludeV1 = false,
|
||||||
|
excludeMms = false
|
||||||
|
).use {
|
||||||
|
assertEquals(2, it.cursor?.count)
|
||||||
|
|
||||||
|
val firstGroup = it.getNext()
|
||||||
|
val secondGroup = it.getNext()
|
||||||
|
|
||||||
|
assertEquals("Group Alice", firstGroup?.title)
|
||||||
|
assertEquals("Group Bob", secondGroup?.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForAnUnsharedToken_thenIExpectOneGroup() {
|
||||||
|
insertPushGroup("Group Alice")
|
||||||
|
insertPushGroup("Group Bob")
|
||||||
|
|
||||||
|
SignalDatabase.groups.queryGroupsByTitle(
|
||||||
|
inputQuery = "Alice",
|
||||||
|
includeInactive = false,
|
||||||
|
excludeV1 = false,
|
||||||
|
excludeMms = false
|
||||||
|
).use {
|
||||||
|
assertEquals(1, it.cursor?.count)
|
||||||
|
|
||||||
|
val firstGroup = it.getNext()
|
||||||
|
|
||||||
|
assertEquals("Group Alice", firstGroup?.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupWithThreeTokens_whenISearchForTheFirstAndLastToken_thenIExpectThatGroup() {
|
||||||
|
insertPushGroup("Group & Alice")
|
||||||
|
|
||||||
|
SignalDatabase.groups.queryGroupsByTitle(
|
||||||
|
inputQuery = "Group Alice",
|
||||||
|
includeInactive = false,
|
||||||
|
excludeV1 = false,
|
||||||
|
excludeMms = false
|
||||||
|
).use {
|
||||||
|
assertEquals(1, it.cursor?.count)
|
||||||
|
|
||||||
|
val firstGroup = it.getNext()
|
||||||
|
|
||||||
|
assertEquals("Group & Alice", firstGroup?.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoGroupsWithSharedTokens_whenISearchForAnExactMatch_thenIExpectThatGroupFirst() {
|
||||||
|
insertPushGroup("Group Alice Bob")
|
||||||
|
insertPushGroup("Group Bob")
|
||||||
|
|
||||||
|
SignalDatabase.groups.queryGroupsByTitle(
|
||||||
|
inputQuery = "Group Bob",
|
||||||
|
includeInactive = false,
|
||||||
|
excludeV1 = false,
|
||||||
|
excludeMms = false
|
||||||
|
).use {
|
||||||
|
assertEquals(2, it.cursor?.count)
|
||||||
|
|
||||||
|
val firstGroup = it.getNext()
|
||||||
|
val second = it.getNext()
|
||||||
|
|
||||||
|
assertEquals("Group Bob", firstGroup?.title)
|
||||||
|
assertEquals("Group Alice Bob", second?.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun insertThread(groupId: GroupId): Long {
|
private fun insertThread(groupId: GroupId): Long {
|
||||||
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
|
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
|
||||||
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
|
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
|
||||||
@@ -292,50 +293,52 @@ class GroupTableTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun insertPushGroup(
|
private fun insertPushGroup(
|
||||||
|
title: String = "Test Group",
|
||||||
members: List<DecryptedMember> = listOf(
|
members: List<DecryptedMember> = listOf(
|
||||||
DecryptedMember.newBuilder()
|
DecryptedMember.Builder()
|
||||||
.setAciBytes(harness.self.requireAci().toByteString())
|
.aciBytes(harness.self.requireAci().toByteString())
|
||||||
.setJoinedAtRevision(0)
|
.joinedAtRevision(0)
|
||||||
.setRole(Member.Role.DEFAULT)
|
.role(Member.Role.DEFAULT)
|
||||||
.build(),
|
.build(),
|
||||||
DecryptedMember.newBuilder()
|
DecryptedMember.Builder()
|
||||||
.setAciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
|
.aciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
|
||||||
.setJoinedAtRevision(0)
|
.joinedAtRevision(0)
|
||||||
.setRole(Member.Role.DEFAULT)
|
.role(Member.Role.DEFAULT)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
): GroupId {
|
): GroupId {
|
||||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
val decryptedGroupState = DecryptedGroup.Builder()
|
||||||
.addAllMembers(members)
|
.title(title)
|
||||||
.setRevision(0)
|
.members(members)
|
||||||
|
.revision(0)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return groupTable.create(groupMasterKey, decryptedGroupState)!!
|
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
|
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
|
||||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||||
|
|
||||||
val selfMember: DecryptedMember = DecryptedMember.newBuilder()
|
val selfMember: DecryptedMember = DecryptedMember.Builder()
|
||||||
.setAciBytes(harness.self.requireAci().toByteString())
|
.aciBytes(harness.self.requireAci().toByteString())
|
||||||
.setJoinedAtRevision(0)
|
.joinedAtRevision(0)
|
||||||
.setRole(Member.Role.DEFAULT)
|
.role(Member.Role.DEFAULT)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val otherMembers: List<DecryptedMember> = others.map { id ->
|
val otherMembers: List<DecryptedMember> = others.map { id ->
|
||||||
DecryptedMember.newBuilder()
|
DecryptedMember.Builder()
|
||||||
.setAciBytes(Recipient.resolved(id).requireAci().toByteString())
|
.aciBytes(Recipient.resolved(id).requireAci().toByteString())
|
||||||
.setJoinedAtRevision(0)
|
.joinedAtRevision(0)
|
||||||
.setRole(Member.Role.DEFAULT)
|
.role(Member.Role.DEFAULT)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
val decryptedGroupState = DecryptedGroup.Builder()
|
||||||
.addAllMembers(listOf(selfMember) + otherMembers)
|
.members(listOf(selfMember) + otherMembers)
|
||||||
.setRevision(0)
|
.revision(0)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return groupTable.create(groupMasterKey, decryptedGroupState)!!
|
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class KyberPreKeyTableTest {
|
|||||||
val count = SignalDatabase.rawDatabase
|
val count = SignalDatabase.rawDatabase
|
||||||
.update(KyberPreKeyTable.TABLE_NAME)
|
.update(KyberPreKeyTable.TABLE_NAME)
|
||||||
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
|
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
|
||||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
|
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
assertEquals(1, count)
|
assertEquals(1, count)
|
||||||
@@ -169,8 +169,15 @@ class KyberPreKeyTableTest {
|
|||||||
return SignalDatabase.rawDatabase
|
return SignalDatabase.rawDatabase
|
||||||
.select(KyberPreKeyTable.STALE_TIMESTAMP)
|
.select(KyberPreKeyTable.STALE_TIMESTAMP)
|
||||||
.from(KyberPreKeyTable.TABLE_NAME)
|
.from(KyberPreKeyTable.TABLE_NAME)
|
||||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
|
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||||
.run()
|
.run()
|
||||||
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
|
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ServiceId.toAccountId(): String {
|
||||||
|
return when (this) {
|
||||||
|
is ACI -> this.toString()
|
||||||
|
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import org.signal.core.util.forEach
|
|||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
import org.signal.core.util.update
|
import org.signal.core.util.updateAll
|
||||||
import org.thoughtcrime.securesms.crash.CrashConfig
|
import org.thoughtcrime.securesms.crash.CrashConfig
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.testing.assertIs
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
|
|
||||||
class LogDatabaseTest {
|
class LogDatabaseTest {
|
||||||
|
|
||||||
private val db: LogDatabase = LogDatabase.getInstance(ApplicationDependencies.getApplication())
|
private val db: LogDatabase = LogDatabase.getInstance(AppDependencies.application)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun crashTable_matchesNamePattern() {
|
fun crashTable_matchesNamePattern() {
|
||||||
@@ -220,7 +220,7 @@ class LogDatabaseTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
db.writableDatabase
|
db.writableDatabase
|
||||||
.update(LogDatabase.CrashTable.TABLE_NAME)
|
.updateAll(LogDatabase.CrashTable.TABLE_NAME)
|
||||||
.values(LogDatabase.CrashTable.LAST_PROMPTED_AT to currentTime)
|
.values(LogDatabase.CrashTable.LAST_PROMPTED_AT to currentTime)
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class MessageTableTest_gifts {
|
|||||||
|
|
||||||
mms.deleteAllThreads()
|
mms.deleteAllThreads()
|
||||||
|
|
||||||
SignalStore.account().setAci(localAci)
|
SignalStore.account.setAci(localAci)
|
||||||
SignalStore.account().setPni(localPni)
|
SignalStore.account.setPni(localPni)
|
||||||
|
|
||||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ class MessageTableTest_gifts {
|
|||||||
val messageId = MmsHelper.insert(
|
val messageId = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 1,
|
sentTimeMillis = 1,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||||
@@ -62,7 +62,7 @@ class MessageTableTest_gifts {
|
|||||||
val messageId = MmsHelper.insert(
|
val messageId = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 1,
|
sentTimeMillis = 1,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
mms.setOutgoingGiftsRevealed(listOf(messageId))
|
mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||||
|
|
||||||
@@ -76,13 +76,13 @@ class MessageTableTest_gifts {
|
|||||||
val messageId = MmsHelper.insert(
|
val messageId = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 1,
|
sentTimeMillis = 1,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 2,
|
sentTimeMillis = 2,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||||
@@ -96,13 +96,13 @@ class MessageTableTest_gifts {
|
|||||||
val messageId = MmsHelper.insert(
|
val messageId = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 1,
|
sentTimeMillis = 1,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
val messageId2 = MmsHelper.insert(
|
val messageId2 = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 2,
|
sentTimeMillis = 2,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
|
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
|
||||||
@@ -115,13 +115,13 @@ class MessageTableTest_gifts {
|
|||||||
val messageId = MmsHelper.insert(
|
val messageId = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 1,
|
sentTimeMillis = 1,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
val messageId2 = MmsHelper.insert(
|
val messageId2 = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 2,
|
sentTimeMillis = 2,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
@@ -140,13 +140,13 @@ class MessageTableTest_gifts {
|
|||||||
val messageId = MmsHelper.insert(
|
val messageId = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 1,
|
sentTimeMillis = 1,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
val messageId2 = MmsHelper.insert(
|
val messageId2 = MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 2,
|
sentTimeMillis = 2,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
val messageId3 = MmsHelper.insert(
|
val messageId3 = MmsHelper.insert(
|
||||||
@@ -165,13 +165,13 @@ class MessageTableTest_gifts {
|
|||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 1,
|
sentTimeMillis = 1,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
recipient = Recipient.resolved(recipients[0]),
|
recipient = Recipient.resolved(recipients[0]),
|
||||||
sentTimeMillis = 2,
|
sentTimeMillis = 2,
|
||||||
giftBadge = GiftBadge.getDefaultInstance()
|
giftBadge = GiftBadge()
|
||||||
)
|
)
|
||||||
|
|
||||||
val messageId3 = MmsHelper.insert(
|
val messageId3 = MmsHelper.insert(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||||
import org.thoughtcrime.securesms.database.model.StoryType
|
import org.thoughtcrime.securesms.database.model.StoryType
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
@@ -55,9 +55,9 @@ object MmsHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun insert(
|
fun insert(
|
||||||
message: IncomingMediaMessage,
|
message: IncomingMessage,
|
||||||
threadId: Long
|
threadId: Long
|
||||||
): Optional<MessageTable.InsertResult> {
|
): Optional<MessageTable.InsertResult> {
|
||||||
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, threadId)
|
return SignalDatabase.messages.insertMessageInbox(message, threadId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.database.model.DistributionListId
|
|||||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||||
import org.thoughtcrime.securesms.database.model.StoryType
|
import org.thoughtcrime.securesms.database.model.StoryType
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
@@ -40,14 +40,14 @@ class MmsTableTest_stories {
|
|||||||
|
|
||||||
mms.deleteAllThreads()
|
mms.deleteAllThreads()
|
||||||
|
|
||||||
SignalStore.account().setAci(localAci)
|
SignalStore.account.setAci(localAci)
|
||||||
SignalStore.account().setPni(localPni)
|
SignalStore.account.setPni(localPni)
|
||||||
|
|
||||||
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
|
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
|
||||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
||||||
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
|
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
|
||||||
|
|
||||||
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)
|
SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelRecipient.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -73,7 +73,8 @@ class MmsTableTest_stories {
|
|||||||
)
|
)
|
||||||
|
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
IncomingMediaMessage(
|
IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = sender,
|
from = sender,
|
||||||
sentTimeMillis = 2,
|
sentTimeMillis = 2,
|
||||||
serverTimeMillis = 2,
|
serverTimeMillis = 2,
|
||||||
@@ -95,7 +96,8 @@ class MmsTableTest_stories {
|
|||||||
// GIVEN
|
// GIVEN
|
||||||
val sender = recipients[0]
|
val sender = recipients[0]
|
||||||
val messageId = MmsHelper.insert(
|
val messageId = MmsHelper.insert(
|
||||||
IncomingMediaMessage(
|
IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = sender,
|
from = sender,
|
||||||
sentTimeMillis = 2,
|
sentTimeMillis = 2,
|
||||||
serverTimeMillis = 2,
|
serverTimeMillis = 2,
|
||||||
@@ -122,7 +124,8 @@ class MmsTableTest_stories {
|
|||||||
// GIVEN
|
// GIVEN
|
||||||
val messageIds = recipients.take(5).map {
|
val messageIds = recipients.take(5).map {
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
IncomingMediaMessage(
|
IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = it,
|
from = it,
|
||||||
sentTimeMillis = 2,
|
sentTimeMillis = 2,
|
||||||
serverTimeMillis = 2,
|
serverTimeMillis = 2,
|
||||||
@@ -154,7 +157,8 @@ class MmsTableTest_stories {
|
|||||||
val unviewedIds: List<Long> = (0 until 5).map {
|
val unviewedIds: List<Long> = (0 until 5).map {
|
||||||
Thread.sleep(5)
|
Thread.sleep(5)
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
IncomingMediaMessage(
|
IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = recipients[it],
|
from = recipients[it],
|
||||||
sentTimeMillis = System.currentTimeMillis(),
|
sentTimeMillis = System.currentTimeMillis(),
|
||||||
serverTimeMillis = 2,
|
serverTimeMillis = 2,
|
||||||
@@ -168,7 +172,8 @@ class MmsTableTest_stories {
|
|||||||
val viewedIds: List<Long> = (0 until 5).map {
|
val viewedIds: List<Long> = (0 until 5).map {
|
||||||
Thread.sleep(5)
|
Thread.sleep(5)
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
IncomingMediaMessage(
|
IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = recipients[it],
|
from = recipients[it],
|
||||||
sentTimeMillis = System.currentTimeMillis(),
|
sentTimeMillis = System.currentTimeMillis(),
|
||||||
serverTimeMillis = 2,
|
serverTimeMillis = 2,
|
||||||
@@ -213,7 +218,8 @@ class MmsTableTest_stories {
|
|||||||
fun givenNoOutgoingStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
|
fun givenNoOutgoingStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
IncomingMediaMessage(
|
IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = recipients[0],
|
from = recipients[0],
|
||||||
sentTimeMillis = 200,
|
sentTimeMillis = 200,
|
||||||
serverTimeMillis = 2,
|
serverTimeMillis = 2,
|
||||||
@@ -321,7 +327,8 @@ class MmsTableTest_stories {
|
|||||||
)
|
)
|
||||||
|
|
||||||
MmsHelper.insert(
|
MmsHelper.insert(
|
||||||
IncomingMediaMessage(
|
IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = myStory.id,
|
from = myStory.id,
|
||||||
sentTimeMillis = 201,
|
sentTimeMillis = 201,
|
||||||
serverTimeMillis = 201,
|
serverTimeMillis = 201,
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.storageservice.protos.groups.Member
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||||
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class NameCollisionTablesTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule(createGroup = true)
|
||||||
|
|
||||||
|
private lateinit var alice: RecipientId
|
||||||
|
private lateinit var bob: RecipientId
|
||||||
|
private lateinit var charlie: RecipientId
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
alice = setUpRecipient(harness.others[0])
|
||||||
|
bob = setUpRecipient(harness.others[1])
|
||||||
|
charlie = setUpRecipient(harness.others[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAUserWithAThreadIdButNoConflicts_whenIGetCollisionsForThreadRecipient_thenIExpectNoCollisions() {
|
||||||
|
val threadRecipientId = alice
|
||||||
|
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(threadRecipientId))
|
||||||
|
val actual = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(threadRecipientId)
|
||||||
|
|
||||||
|
actual assertIsSize 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoUsers_whenOneChangesTheirProfileNameToMatchTheOther_thenIExpectANameCollision() {
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
|
||||||
|
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
|
||||||
|
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||||
|
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
|
||||||
|
|
||||||
|
actualAlice assertIsSize 2
|
||||||
|
actualBob assertIsSize 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectNoNameCollisions() {
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
|
||||||
|
|
||||||
|
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||||
|
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
|
||||||
|
|
||||||
|
actualAlice assertIsSize 0
|
||||||
|
actualBob assertIsSize 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenThreeUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectTwoNameCollisions() {
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(charlie, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
|
||||||
|
|
||||||
|
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||||
|
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
|
||||||
|
val actualCharlie = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(charlie)
|
||||||
|
|
||||||
|
actualAlice assertIsSize 0
|
||||||
|
actualBob assertIsSize 2
|
||||||
|
actualCharlie assertIsSize 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoUsersWithADismissedNameCollision_whenOneChangesToADifferentNameAndBack_thenIExpectANameCollision() {
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
|
||||||
|
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
|
||||||
|
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||||
|
|
||||||
|
actualAlice assertIsSize 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
|
||||||
|
|
||||||
|
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||||
|
|
||||||
|
actualCollisions assertIsSize 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenADismissedNameCollisionForAliceThatIUpdate_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
|
||||||
|
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
|
||||||
|
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
|
||||||
|
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
|
||||||
|
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||||
|
|
||||||
|
actualCollisions assertIsSize 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForBob_thenIExpectANameCollisionWithTwoEntries() {
|
||||||
|
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
|
||||||
|
|
||||||
|
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
|
||||||
|
|
||||||
|
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
|
||||||
|
|
||||||
|
actualCollisions assertIsSize 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
|
||||||
|
val alice = Recipient.resolved(alice)
|
||||||
|
val bob = Recipient.resolved(bob)
|
||||||
|
val info = createGroup()
|
||||||
|
|
||||||
|
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
|
||||||
|
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
|
||||||
|
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
|
||||||
|
|
||||||
|
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
|
||||||
|
|
||||||
|
collisions assertIsSize 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupWithAliceAndBobWithDismissedCollision_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
|
||||||
|
val alice = Recipient.resolved(alice)
|
||||||
|
val bob = Recipient.resolved(bob)
|
||||||
|
val info = createGroup()
|
||||||
|
|
||||||
|
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
|
||||||
|
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
|
||||||
|
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
|
||||||
|
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(info.recipientId)
|
||||||
|
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
|
||||||
|
|
||||||
|
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
|
||||||
|
|
||||||
|
collisions assertIsSize 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAliceWithMismatch_thenIExpectNoGroupNameCollision() {
|
||||||
|
val alice = Recipient.resolved(alice)
|
||||||
|
val bob = Recipient.resolved(bob)
|
||||||
|
val info = createGroup()
|
||||||
|
|
||||||
|
setProfileName(alice.id, ProfileName.fromParts("Alice", "Android"))
|
||||||
|
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
|
||||||
|
|
||||||
|
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
|
||||||
|
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Alice Android", "Bob Android")
|
||||||
|
|
||||||
|
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
|
||||||
|
|
||||||
|
collisions assertIsSize 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpRecipient(recipientId: RecipientId): RecipientId {
|
||||||
|
SignalDatabase.recipients.setProfileSharing(recipientId, false)
|
||||||
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
|
||||||
|
|
||||||
|
MmsHelper.insert(
|
||||||
|
threadId = threadId,
|
||||||
|
message = IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
|
from = recipientId,
|
||||||
|
groupId = null,
|
||||||
|
body = "hi",
|
||||||
|
sentTimeMillis = 100L,
|
||||||
|
receivedTimeMillis = 200L,
|
||||||
|
serverTimeMillis = 100L,
|
||||||
|
isUnidentified = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return recipientId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setProfileName(recipientId: RecipientId, name: ProfileName) {
|
||||||
|
SignalDatabase.recipients.setProfileName(recipientId, name)
|
||||||
|
SignalDatabase.nameCollisions.handleIndividualNameCollision(recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createGroup(): GroupTestingUtils.TestGroupInfo {
|
||||||
|
return GroupTestingUtils.insertGroup(
|
||||||
|
revision = 0,
|
||||||
|
DecryptedMember(
|
||||||
|
aciBytes = harness.self.requireAci().toByteString(),
|
||||||
|
role = Member.Role.ADMINISTRATOR
|
||||||
|
),
|
||||||
|
DecryptedMember(
|
||||||
|
aciBytes = Recipient.resolved(alice).requireAci().toByteString(),
|
||||||
|
role = Member.Role.ADMINISTRATOR
|
||||||
|
),
|
||||||
|
DecryptedMember(
|
||||||
|
aciBytes = Recipient.resolved(bob).requireAci().toByteString(),
|
||||||
|
role = Member.Role.ADMINISTRATOR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,7 +120,7 @@ class OneTimePreKeyTableTest {
|
|||||||
val count = SignalDatabase.rawDatabase
|
val count = SignalDatabase.rawDatabase
|
||||||
.update(OneTimePreKeyTable.TABLE_NAME)
|
.update(OneTimePreKeyTable.TABLE_NAME)
|
||||||
.values(OneTimePreKeyTable.STALE_TIMESTAMP to staleTime)
|
.values(OneTimePreKeyTable.STALE_TIMESTAMP to staleTime)
|
||||||
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
|
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
assertEquals(1, count)
|
assertEquals(1, count)
|
||||||
@@ -130,8 +130,15 @@ class OneTimePreKeyTableTest {
|
|||||||
return SignalDatabase.rawDatabase
|
return SignalDatabase.rawDatabase
|
||||||
.select(OneTimePreKeyTable.STALE_TIMESTAMP)
|
.select(OneTimePreKeyTable.STALE_TIMESTAMP)
|
||||||
.from(OneTimePreKeyTable.TABLE_NAME)
|
.from(OneTimePreKeyTable.TABLE_NAME)
|
||||||
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
|
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||||
.run()
|
.run()
|
||||||
.readToSingleObject { it.requireLongOrNull(OneTimePreKeyTable.STALE_TIMESTAMP) }
|
.readToSingleObject { it.requireLongOrNull(OneTimePreKeyTable.STALE_TIMESTAMP) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ServiceId.toAccountId(): String {
|
||||||
|
return when (this) {
|
||||||
|
is ACI -> this.toString()
|
||||||
|
is PNI -> OneTimePreKeyTable.PNI_ACCOUNT_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import org.signal.core.util.CursorUtil
|
|||||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -59,7 +57,7 @@ class RecipientTableTest {
|
|||||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||||
|
|
||||||
val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!!
|
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Hidden", false))!!
|
||||||
|
|
||||||
assertEquals(0, results.count)
|
assertEquals(0, results.count)
|
||||||
}
|
}
|
||||||
@@ -130,7 +128,7 @@ class RecipientTableTest {
|
|||||||
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
||||||
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
||||||
|
|
||||||
val results = SignalDatabase.recipients.querySignalContacts("Blocked", false)!!
|
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", false))!!
|
||||||
|
|
||||||
assertEquals(0, results.count)
|
assertEquals(0, results.count)
|
||||||
}
|
}
|
||||||
@@ -167,8 +165,6 @@ class RecipientTableTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
|
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
|
||||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
|
||||||
|
|
||||||
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||||
|
|
||||||
SignalDatabase.recipients.markUnregistered(mainId)
|
SignalDatabase.recipients.markUnregistered(mainId)
|
||||||
@@ -185,12 +181,10 @@ class RecipientTableTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
|
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
|
||||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
|
||||||
|
|
||||||
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||||
val mainRecord = SignalDatabase.recipients.getRecord(mainId)
|
val mainRecord = SignalDatabase.recipients.getRecord(mainId)
|
||||||
|
|
||||||
SignalDatabase.recipients.splitForStorageSync(mainRecord.storageId!!)
|
SignalDatabase.recipients.splitForStorageSyncIfNecessary(mainRecord.aci!!)
|
||||||
|
|
||||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||||
@@ -28,7 +28,7 @@ class RecipientTableTest_applyStorageSyncContactUpdate {
|
|||||||
@Test
|
@Test
|
||||||
fun insertMessageOnVerifiedToDefault() {
|
fun insertMessageOnVerifiedToDefault() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val identities = ApplicationDependencies.getProtocolStore().aci().identities()
|
val identities = AppDependencies.protocolStore.aci().identities()
|
||||||
val other = Recipient.resolved(harness.others[0])
|
val other = Recipient.resolved(harness.others[0])
|
||||||
|
|
||||||
MmsHelper.insert(recipient = other)
|
MmsHelper.insert(recipient = other)
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import org.junit.Assert.assertTrue
|
|||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.SqlUtil
|
import org.signal.core.util.SqlUtil
|
||||||
import org.signal.core.util.exists
|
import org.signal.core.util.exists
|
||||||
|
import org.signal.core.util.orNull
|
||||||
|
import org.signal.core.util.readToSingleBoolean
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
@@ -31,17 +34,13 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
|
||||||
import org.thoughtcrime.securesms.util.Base64
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
@@ -54,10 +53,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
SignalStore.account().setE164(E164_SELF)
|
SignalStore.account.setE164(E164_SELF)
|
||||||
SignalStore.account().setAci(ACI_SELF)
|
SignalStore.account.setAci(ACI_SELF)
|
||||||
SignalStore.account().setPni(PNI_SELF)
|
SignalStore.account.setPni(PNI_SELF)
|
||||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -109,6 +107,18 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
val record = SignalDatabase.recipients.getRecord(id)
|
val record = SignalDatabase.recipients.getRecord(id)
|
||||||
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("e164+pni+aci insert, pni verified") {
|
||||||
|
val id = process(E164_A, PNI_A, ACI_A, pniVerified = true)
|
||||||
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
expectPniVerified()
|
||||||
|
|
||||||
|
val record = SignalDatabase.recipients.getRecord(id)
|
||||||
|
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
||||||
|
|
||||||
|
process(E164_A, PNI_A, ACI_A, pniVerified = false)
|
||||||
|
expectPniVerified()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -142,6 +152,31 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
process(null, null, null)
|
process(null, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("pni matches, pni+aci provided, no pni session") {
|
||||||
|
given(E164_A, PNI_A, null)
|
||||||
|
process(null, PNI_A, ACI_A)
|
||||||
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
|
||||||
|
expectNoSessionSwitchoverEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
test("pni matches, pni+aci provided, pni session") {
|
||||||
|
given(E164_A, PNI_A, null, pniSession = true)
|
||||||
|
process(null, PNI_A, ACI_A)
|
||||||
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
|
||||||
|
expectSessionSwitchoverEvent(E164_A)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("pni matches, pni+aci provided, pni session, pni-verified") {
|
||||||
|
given(E164_A, PNI_A, null, pniSession = true)
|
||||||
|
process(null, PNI_A, ACI_A, pniVerified = true)
|
||||||
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
|
||||||
|
expectNoSessionSwitchoverEvent()
|
||||||
|
expectPniVerified()
|
||||||
|
}
|
||||||
|
|
||||||
test("no match, all fields") {
|
test("no match, all fields") {
|
||||||
process(E164_A, PNI_A, ACI_A)
|
process(E164_A, PNI_A, ACI_A)
|
||||||
expect(E164_A, PNI_A, ACI_A)
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
@@ -201,6 +236,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
given(E164_A, PNI_A, null, pniSession = true)
|
given(E164_A, PNI_A, null, pniSession = true)
|
||||||
process(E164_A, PNI_A, ACI_A, pniVerified = true)
|
process(E164_A, PNI_A, ACI_A, pniVerified = true)
|
||||||
expect(E164_A, PNI_A, ACI_A)
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
|
||||||
|
expectPniVerified()
|
||||||
}
|
}
|
||||||
|
|
||||||
test("e164 and aci matches, all provided, new pni") {
|
test("e164 and aci matches, all provided, new pni") {
|
||||||
@@ -502,6 +539,18 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
expectNoSessionSwitchoverEvent()
|
expectNoSessionSwitchoverEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("steal, e164+pni+aci * pni+aci, all provided, aci sessions but not pni sessions, no SSE expected") {
|
||||||
|
given(E164_A, PNI_A, ACI_A, createThread = true, aciSession = true, pniSession = false)
|
||||||
|
given(null, PNI_B, ACI_B, createThread = false, aciSession = true, pniSession = false)
|
||||||
|
|
||||||
|
process(E164_A, PNI_B, ACI_A)
|
||||||
|
|
||||||
|
expect(E164_A, PNI_B, ACI_A)
|
||||||
|
expect(null, null, ACI_B)
|
||||||
|
|
||||||
|
expectNoSessionSwitchoverEvent()
|
||||||
|
}
|
||||||
|
|
||||||
test("merge, e164 & pni & aci, all provided") {
|
test("merge, e164 & pni & aci, all provided") {
|
||||||
given(E164_A, null, null)
|
given(E164_A, null, null)
|
||||||
given(null, PNI_A, null)
|
given(null, PNI_A, null)
|
||||||
@@ -658,6 +707,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
|
|
||||||
expectDeleted()
|
expectDeleted()
|
||||||
expect(E164_A, PNI_A, ACI_A)
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
|
||||||
|
expectPniVerified()
|
||||||
}
|
}
|
||||||
|
|
||||||
test("merge, e164+pni & aci, pni session, pni verified") {
|
test("merge, e164+pni & aci, pni session, pni verified") {
|
||||||
@@ -670,6 +721,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
expect(E164_A, PNI_A, ACI_A)
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
|
||||||
expectThreadMergeEvent(E164_A)
|
expectThreadMergeEvent(E164_A)
|
||||||
|
expectPniVerified()
|
||||||
}
|
}
|
||||||
|
|
||||||
test("merge, e164+pni & e164+pni+aci, change number") {
|
test("merge, e164+pni & e164+pni+aci, change number") {
|
||||||
@@ -724,6 +776,18 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
expectThreadMergeEvent(E164_A)
|
expectThreadMergeEvent(E164_A)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("merge, e164+pni & e164+aci, pni+aci provided, change number") {
|
||||||
|
given(E164_A, PNI_A, null)
|
||||||
|
given(E164_B, null, ACI_A)
|
||||||
|
|
||||||
|
process(null, PNI_A, ACI_A)
|
||||||
|
|
||||||
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
|
||||||
|
expectThreadMergeEvent(E164_A)
|
||||||
|
expectChangeNumberEvent()
|
||||||
|
}
|
||||||
|
|
||||||
test("merge, e164 + pni reassigned, aci abandoned") {
|
test("merge, e164 + pni reassigned, aci abandoned") {
|
||||||
given(E164_A, PNI_A, ACI_A)
|
given(E164_A, PNI_A, ACI_A)
|
||||||
given(E164_B, PNI_B, ACI_B)
|
given(E164_B, PNI_B, ACI_B)
|
||||||
@@ -736,6 +800,17 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
expectChangeNumberEvent()
|
expectChangeNumberEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("merge, e164 follows pni+aci") {
|
||||||
|
given(E164_A, PNI_A, null)
|
||||||
|
given(null, null, ACI_A)
|
||||||
|
|
||||||
|
process(null, PNI_A, ACI_A, pniVerified = true)
|
||||||
|
|
||||||
|
expect(E164_A, PNI_A, ACI_A)
|
||||||
|
expectThreadMergeEvent(E164_A)
|
||||||
|
expectPniVerified()
|
||||||
|
}
|
||||||
|
|
||||||
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
|
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
|
||||||
given(E164_SELF, null, ACI_SELF)
|
given(E164_SELF, null, ACI_SELF)
|
||||||
given(null, null, ACI_A)
|
given(null, null, ACI_A)
|
||||||
@@ -789,9 +864,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
val smsId2: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
|
val smsId2: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
|
||||||
val smsId3: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
|
val smsId3: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
|
||||||
|
|
||||||
val mmsId1: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
|
val mmsId1: Long = SignalDatabase.messages.insertMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
|
||||||
val mmsId2: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
|
val mmsId2: Long = SignalDatabase.messages.insertMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
|
||||||
val mmsId3: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
|
val mmsId3: Long = SignalDatabase.messages.insertMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
|
||||||
|
|
||||||
val threadIdAci: Long = SignalDatabase.threads.getThreadIdFor(recipientIdAci)!!
|
val threadIdAci: Long = SignalDatabase.threads.getThreadIdFor(recipientIdAci)!!
|
||||||
val threadIdE164: Long = SignalDatabase.threads.getThreadIdFor(recipientIdE164)!!
|
val threadIdE164: Long = SignalDatabase.threads.getThreadIdFor(recipientIdE164)!!
|
||||||
@@ -838,8 +913,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
|
|
||||||
// Thread validation
|
// Thread validation
|
||||||
assertEquals(threadIdAci, retrievedThreadId)
|
assertEquals(threadIdAci, retrievedThreadId)
|
||||||
Assert.assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
|
assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
|
||||||
Assert.assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
|
assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
|
||||||
|
|
||||||
// SMS validation
|
// SMS validation
|
||||||
val sms1: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId1)!!
|
val sms1: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId1)!!
|
||||||
@@ -883,10 +958,10 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
|
|
||||||
// Identity validation
|
// Identity validation
|
||||||
assertEquals(identityKeyAci, SignalDatabase.identities.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
|
assertEquals(identityKeyAci, SignalDatabase.identities.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
|
||||||
Assert.assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
|
assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
|
||||||
|
|
||||||
// Session validation
|
// Session validation
|
||||||
Assert.assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
|
assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
|
||||||
|
|
||||||
// Reaction validation
|
// Reaction validation
|
||||||
val reactionsSms: List<ReactionRecord> = SignalDatabase.reactions.getReactions(MessageId(smsId1))
|
val reactionsSms: List<ReactionRecord> = SignalDatabase.reactions.getReactions(MessageId(smsId1))
|
||||||
@@ -911,12 +986,30 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
|
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
|
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMessage {
|
||||||
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
|
return IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
|
from = sender,
|
||||||
|
sentTimeMillis = time,
|
||||||
|
serverTimeMillis = time,
|
||||||
|
receivedTimeMillis = time,
|
||||||
|
body = body,
|
||||||
|
groupId = groupId.orNull(),
|
||||||
|
isUnidentified = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
|
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMessage {
|
||||||
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty(), false, false)
|
return IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
|
from = sender,
|
||||||
|
groupId = groupId.orNull(),
|
||||||
|
body = body,
|
||||||
|
sentTimeMillis = time,
|
||||||
|
receivedTimeMillis = time,
|
||||||
|
serverTimeMillis = time,
|
||||||
|
isUnidentified = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun identityKey(value: Byte): IdentityKey {
|
private fun identityKey(value: Byte): IdentityKey {
|
||||||
@@ -983,6 +1076,10 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
if (!test.sessionSwitchoverExpected) {
|
if (!test.sessionSwitchoverExpected) {
|
||||||
test.expectNoSessionSwitchoverEvent()
|
test.expectNoSessionSwitchoverEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!test.pniVerifiedExpected) {
|
||||||
|
test.expectPniNotVerified()
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (e.javaClass != exception) {
|
if (e.javaClass != exception) {
|
||||||
val error = java.lang.AssertionError("[$name] ${e.message}")
|
val error = java.lang.AssertionError("[$name] ${e.message}")
|
||||||
@@ -1002,6 +1099,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
var changeNumberExpected = false
|
var changeNumberExpected = false
|
||||||
var threadMergeExpected = false
|
var threadMergeExpected = false
|
||||||
var sessionSwitchoverExpected = false
|
var sessionSwitchoverExpected = false
|
||||||
|
var pniVerifiedExpected = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Need to delete these first to prevent foreign key crash
|
// Need to delete these first to prevent foreign key crash
|
||||||
@@ -1015,8 +1113,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
|
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationDependencies.getRecipientCache().clear()
|
AppDependencies.recipientCache.clear()
|
||||||
ApplicationDependencies.getRecipientCache().clearSelf()
|
AppDependencies.recipientCache.clearSelf()
|
||||||
RecipientId.clearCache()
|
RecipientId.clearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,6 +1251,24 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
assertNull("Unexpected thread merge event!", getLatestThreadMergeEvent(outputRecipientId))
|
assertNull("Unexpected thread merge event!", getLatestThreadMergeEvent(outputRecipientId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun expectPniVerified() {
|
||||||
|
assertTrue("Expected PNI to be verified!", isPniVerified(outputRecipientId))
|
||||||
|
pniVerifiedExpected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun expectPniNotVerified() {
|
||||||
|
assertFalse("Expected PNI to be not be verified!", isPniVerified(outputRecipientId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isPniVerified(recipientId: RecipientId): Boolean {
|
||||||
|
return SignalDatabase.rawDatabase
|
||||||
|
.select(RecipientTable.PNI_SIGNATURE_VERIFIED)
|
||||||
|
.from(RecipientTable.TABLE_NAME)
|
||||||
|
.where("${RecipientTable.ID} = ?", recipientId)
|
||||||
|
.run()
|
||||||
|
.readToSingleBoolean(false)
|
||||||
|
}
|
||||||
|
|
||||||
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
||||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||||
RecipientTable.TABLE_NAME,
|
RecipientTable.TABLE_NAME,
|
||||||
@@ -1228,7 +1344,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
.use { cursor: Cursor ->
|
.use { cursor: Cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
|
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
|
||||||
ThreadMergeEvent.parseFrom(bytes)
|
ThreadMergeEvent.ADAPTER.decode(bytes)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -1246,7 +1362,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
.use { cursor: Cursor ->
|
.use { cursor: Cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
|
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
|
||||||
SessionSwitchoverEvent.parseFrom(bytes)
|
SessionSwitchoverEvent.ADAPTER.decode(bytes)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.signal.core.util.Hex
|
import org.signal.core.util.Hex
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||||
|
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
|
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
|
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
|
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
|
||||||
@@ -18,12 +20,10 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.groupChange
|
|||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
|
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
|
|
||||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
import java.util.Optional
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Suppress("ClassName", "TestFunctionName")
|
@Suppress("ClassName", "TestFunctionName")
|
||||||
@@ -46,8 +46,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
|||||||
recipients = SignalDatabase.recipients
|
recipients = SignalDatabase.recipients
|
||||||
sms = SignalDatabase.messages
|
sms = SignalDatabase.messages
|
||||||
|
|
||||||
SignalStore.account().setAci(localAci)
|
SignalStore.account.setAci(localAci)
|
||||||
SignalStore.account().setPni(localPni)
|
SignalStore.account.setPni(localPni)
|
||||||
|
|
||||||
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
|
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
|
||||||
bob = recipients.getOrInsertFromServiceId(bobServiceId)
|
bob = recipients.getOrInsertFromServiceId(bobServiceId)
|
||||||
@@ -272,13 +272,36 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
|||||||
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
|
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingTextMessage {
|
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingMessage {
|
||||||
wallClock++
|
wallClock++
|
||||||
return IncomingTextMessage(sender, 1, wallClock, wallClock, wallClock, body, Optional.of(groupId), 0, true, null)
|
return IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
|
from = sender,
|
||||||
|
sentTimeMillis = wallClock,
|
||||||
|
serverTimeMillis = wallClock,
|
||||||
|
receivedTimeMillis = wallClock,
|
||||||
|
body = body,
|
||||||
|
groupId = groupId,
|
||||||
|
isUnidentified = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingGroupUpdateMessage {
|
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingMessage {
|
||||||
return IncomingGroupUpdateMessage(smsMessage(sender, null), groupContext)
|
wallClock++
|
||||||
|
|
||||||
|
val updateDescription = GV2UpdateDescription(
|
||||||
|
gv2ChangeDescription = groupContext,
|
||||||
|
groupChangeUpdate = GroupsV2UpdateMessageConverter.translateDecryptedChangeUpdate(SignalStore.account.getServiceIds(), groupContext)
|
||||||
|
)
|
||||||
|
|
||||||
|
return IncomingMessage.groupUpdate(
|
||||||
|
from = sender,
|
||||||
|
timestamp = wallClock,
|
||||||
|
groupId = groupId,
|
||||||
|
update = updateDescription,
|
||||||
|
isGroupAdd = false,
|
||||||
|
serverGuid = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
|
|||||||
import org.thoughtcrime.securesms.audio.AudioHash
|
import org.thoughtcrime.securesms.audio.AudioHash
|
||||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
object UriAttachmentBuilder {
|
object UriAttachmentBuilder {
|
||||||
fun build(
|
fun build(
|
||||||
@@ -22,23 +23,28 @@ object UriAttachmentBuilder {
|
|||||||
stickerLocator: StickerLocator? = null,
|
stickerLocator: StickerLocator? = null,
|
||||||
blurHash: BlurHash? = null,
|
blurHash: BlurHash? = null,
|
||||||
audioHash: AudioHash? = null,
|
audioHash: AudioHash? = null,
|
||||||
transformProperties: AttachmentTable.TransformProperties? = null
|
transformProperties: AttachmentTable.TransformProperties? = null,
|
||||||
|
uuid: UUID? = UUID.randomUUID()
|
||||||
): UriAttachment {
|
): UriAttachment {
|
||||||
return UriAttachment(
|
return UriAttachment(
|
||||||
uri,
|
dataUri = uri,
|
||||||
contentType,
|
contentType = contentType,
|
||||||
transferState,
|
transferState = transferState,
|
||||||
size,
|
size = size,
|
||||||
fileName,
|
width = 0,
|
||||||
voiceNote,
|
height = 0,
|
||||||
borderless,
|
fileName = fileName,
|
||||||
videoGif,
|
fastPreflightId = null,
|
||||||
quote,
|
voiceNote = voiceNote,
|
||||||
caption,
|
borderless = borderless,
|
||||||
stickerLocator,
|
videoGif = videoGif,
|
||||||
blurHash,
|
quote = quote,
|
||||||
audioHash,
|
caption = caption,
|
||||||
transformProperties
|
stickerLocator = stickerLocator,
|
||||||
|
blurHash = blurHash,
|
||||||
|
audioHash = audioHash,
|
||||||
|
transformProperties = transformProperties,
|
||||||
|
uuid = uuid
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.database.helpers.migration
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.readToSingleObject
|
||||||
|
import org.signal.core.util.requireNonNullString
|
||||||
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.update
|
||||||
|
import org.signal.donations.InAppPaymentType
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue
|
||||||
|
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
||||||
|
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class FixInAppCurrencyIfAbleTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenNoSubscribers_whenIMigrate_thenIDoNothing() {
|
||||||
|
migrate()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenASubscriberButNoPayment_whenIMigrate_thenIDoNothing() {
|
||||||
|
val subscriber = insertSubscriber("USD")
|
||||||
|
clearCurrencyCode(subscriber)
|
||||||
|
migrate()
|
||||||
|
|
||||||
|
getCurrencyCode(subscriber) assertIs ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenASubscriberAndMismatchedPayment_whenIMigrate_thenIDoNothing() {
|
||||||
|
val subscriber = insertSubscriber("USD")
|
||||||
|
val otherSubscriber = insertSubscriber("EUR")
|
||||||
|
insertPayment(otherSubscriber)
|
||||||
|
clearCurrencyCode(subscriber)
|
||||||
|
migrate()
|
||||||
|
|
||||||
|
getCurrencyCode(subscriber) assertIs ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenASubscriberAndPaymentWithNoSubscriber_whenIMigrate_thenDoNothing() {
|
||||||
|
val subscriber = insertSubscriber("USD")
|
||||||
|
insertPayment(null)
|
||||||
|
clearCurrencyCode(subscriber)
|
||||||
|
migrate()
|
||||||
|
|
||||||
|
getCurrencyCode(subscriber) assertIs ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenASubscriberAndMatchingPayment_whenIMigrate_thenUpdateCurrencyCode() {
|
||||||
|
val subscriber = insertSubscriber("USD")
|
||||||
|
insertPayment(subscriber)
|
||||||
|
clearCurrencyCode(subscriber)
|
||||||
|
migrate()
|
||||||
|
|
||||||
|
getCurrencyCode(subscriber) assertIs "USD"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenASupercededSubscriber_whenIMigrate_thenIDoNothing() {
|
||||||
|
val oldSubscriber = insertSubscriber("USD")
|
||||||
|
insertPayment(oldSubscriber)
|
||||||
|
clearCurrencyCode(oldSubscriber)
|
||||||
|
insertSubscriber("USD")
|
||||||
|
migrate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrate() {
|
||||||
|
V236_FixInAppSubscriberCurrencyIfAble.migrate(
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
|
||||||
|
db = SignalDatabase.rawDatabase,
|
||||||
|
oldVersion = 0,
|
||||||
|
newVersion = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertSubscriber(currencyCode: String): InAppPaymentSubscriberRecord {
|
||||||
|
val record = InAppPaymentSubscriberRecord(
|
||||||
|
subscriberId = SubscriberId.generate(),
|
||||||
|
currency = Currency.getInstance(currencyCode),
|
||||||
|
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||||
|
requiresCancel = false,
|
||||||
|
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(record)
|
||||||
|
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord) {
|
||||||
|
SignalDatabase.rawDatabase.update(InAppPaymentSubscriberTable.TABLE_NAME)
|
||||||
|
.values(InAppPaymentSubscriberTable.CURRENCY_CODE to "")
|
||||||
|
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord): String {
|
||||||
|
return SignalDatabase.rawDatabase.select(InAppPaymentSubscriberTable.CURRENCY_CODE)
|
||||||
|
.from(InAppPaymentSubscriberTable.TABLE_NAME)
|
||||||
|
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
|
||||||
|
.run()
|
||||||
|
.readToSingleObject { it.requireNonNullString(InAppPaymentSubscriberTable.CURRENCY_CODE) }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertPayment(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord?): InAppPaymentTable.InAppPayment {
|
||||||
|
val id = SignalDatabase.inAppPayments.insert(
|
||||||
|
type = InAppPaymentType.RECURRING_DONATION,
|
||||||
|
state = InAppPaymentTable.State.END,
|
||||||
|
subscriberId = inAppPaymentSubscriberRecord?.subscriberId,
|
||||||
|
endOfPeriod = null,
|
||||||
|
inAppPaymentData = InAppPaymentData(
|
||||||
|
amount = FiatValue(
|
||||||
|
currencyCode = inAppPaymentSubscriberRecord?.currency?.currencyCode ?: "USD",
|
||||||
|
amount = BigDecimal.ONE.toDecimalValue()
|
||||||
|
),
|
||||||
|
level = 200,
|
||||||
|
paymentMethodType = inAppPaymentSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return SignalDatabase.inAppPayments.getById(id)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.dependencies
|
package org.thoughtcrime.securesms.dependencies
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import io.mockk.spyk
|
||||||
import okhttp3.ConnectionSpec
|
import okhttp3.ConnectionSpec
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
@@ -13,9 +14,9 @@ import okio.ByteString
|
|||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.doReturn
|
import org.mockito.kotlin.doReturn
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.BuildConfig
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
import org.thoughtcrime.securesms.KbsEnclave
|
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceTrustStore
|
import org.thoughtcrime.securesms.push.SignalServiceTrustStore
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
|
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
|
||||||
@@ -23,33 +24,30 @@ import org.thoughtcrime.securesms.testing.Get
|
|||||||
import org.thoughtcrime.securesms.testing.Verb
|
import org.thoughtcrime.securesms.testing.Verb
|
||||||
import org.thoughtcrime.securesms.testing.runSync
|
import org.thoughtcrime.securesms.testing.runSync
|
||||||
import org.thoughtcrime.securesms.testing.success
|
import org.thoughtcrime.securesms.testing.success
|
||||||
import org.thoughtcrime.securesms.util.Base64
|
import org.whispersystems.signalservice.api.SignalServiceDataStore
|
||||||
import org.whispersystems.signalservice.api.KeyBackupService
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
import org.whispersystems.signalservice.api.SignalWebSocket
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore
|
import org.whispersystems.signalservice.api.push.TrustStore
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
|
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
|
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl
|
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl
|
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
|
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
|
||||||
import java.security.KeyStore
|
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dependency provider used for instrumentation tests (aka androidTests).
|
* Dependency provider used for instrumentation tests (aka androidTests).
|
||||||
*
|
*
|
||||||
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess] and
|
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess].
|
||||||
* [KeyBackupService].
|
|
||||||
*/
|
*/
|
||||||
class InstrumentationApplicationDependencyProvider(application: Application, default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
|
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : AppDependencies.Provider by default {
|
||||||
|
|
||||||
private val serviceTrustStore: TrustStore
|
private val serviceTrustStore: TrustStore
|
||||||
private val uncensoredConfiguration: SignalServiceConfiguration
|
private val uncensoredConfiguration: SignalServiceConfiguration
|
||||||
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
|
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
|
||||||
private val keyBackupService: KeyBackupService
|
|
||||||
private val recipientCache: LiveRecipientCache
|
private val recipientCache: LiveRecipientCache
|
||||||
|
private var signalServiceMessageSender: SignalServiceMessageSender? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
runSync {
|
runSync {
|
||||||
@@ -60,18 +58,21 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
|||||||
Get("/v1/websocket/?login=") {
|
Get("/v1/websocket/?login=") {
|
||||||
MockResponse().success().withWebSocketUpgrade(mockIdentifiedWebSocket)
|
MockResponse().success().withWebSocketUpgrade(mockIdentifiedWebSocket)
|
||||||
},
|
},
|
||||||
Get("/v1/websocket", { !it.path.contains("login") }) {
|
Get("/v1/websocket", {
|
||||||
|
val path = it.path
|
||||||
|
return@Get path == null || !path.contains("login")
|
||||||
|
}) {
|
||||||
MockResponse().success().withWebSocketUpgrade(object : WebSocketListener() {})
|
MockResponse().success().withWebSocketUpgrade(object : WebSocketListener() {})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
webServer.setDispatcher(object : Dispatcher() {
|
webServer.dispatcher = object : Dispatcher() {
|
||||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||||
val handler = handlers.firstOrNull { it.requestPredicate(request) }
|
val handler = handlers.firstOrNull { it.requestPredicate(request) }
|
||||||
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
|
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
serviceTrustStore = SignalServiceTrustStore(application)
|
serviceTrustStore = SignalServiceTrustStore(application)
|
||||||
uncensoredConfiguration = SignalServiceConfiguration(
|
uncensoredConfiguration = SignalServiceConfiguration(
|
||||||
@@ -80,7 +81,6 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
|||||||
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||||
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
|
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
|
||||||
),
|
),
|
||||||
signalKeyBackupServiceUrls = arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
|
||||||
signalStorageUrls = arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
signalStorageUrls = arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||||
signalCdsiUrls = arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
signalCdsiUrls = arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||||
signalSvr2Urls = arrayOf(SignalSvr2Url(baseUrl, serviceTrustStore, "localhost", ConnectionSpec.CLEARTEXT)),
|
signalSvr2Urls = arrayOf(SignalSvr2Url(baseUrl, serviceTrustStore, "localhost", ConnectionSpec.CLEARTEXT)),
|
||||||
@@ -88,7 +88,8 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
|||||||
dns = Optional.of(SignalServiceNetworkAccess.DNS),
|
dns = Optional.of(SignalServiceNetworkAccess.DNS),
|
||||||
signalProxy = Optional.empty(),
|
signalProxy = Optional.empty(),
|
||||||
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
|
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
|
||||||
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS)
|
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS),
|
||||||
|
backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS)
|
||||||
)
|
)
|
||||||
|
|
||||||
serviceNetworkAccessMock = mock {
|
serviceNetworkAccessMock = mock {
|
||||||
@@ -97,8 +98,6 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
|||||||
on { uncensoredConfiguration } doReturn uncensoredConfiguration
|
on { uncensoredConfiguration } doReturn uncensoredConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
keyBackupService = mock()
|
|
||||||
|
|
||||||
recipientCache = LiveRecipientCache(application) { r -> r.run() }
|
recipientCache = LiveRecipientCache(application) { r -> r.run() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,14 +105,21 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
|||||||
return serviceNetworkAccessMock
|
return serviceNetworkAccessMock
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun provideKeyBackupService(signalServiceAccountManager: SignalServiceAccountManager, keyStore: KeyStore, enclave: KbsEnclave): KeyBackupService {
|
|
||||||
return keyBackupService
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun provideRecipientCache(): LiveRecipientCache {
|
override fun provideRecipientCache(): LiveRecipientCache {
|
||||||
return recipientCache
|
return recipientCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun provideSignalServiceMessageSender(
|
||||||
|
signalWebSocket: SignalWebSocket,
|
||||||
|
protocolStore: SignalServiceDataStore,
|
||||||
|
signalServiceConfiguration: SignalServiceConfiguration
|
||||||
|
): SignalServiceMessageSender {
|
||||||
|
if (signalServiceMessageSender == null) {
|
||||||
|
signalServiceMessageSender = spyk(objToCopy = default.provideSignalServiceMessageSender(signalWebSocket, protocolStore, signalServiceConfiguration))
|
||||||
|
}
|
||||||
|
return signalServiceMessageSender!!
|
||||||
|
}
|
||||||
|
|
||||||
class MockWebSocket : WebSocketListener() {
|
class MockWebSocket : WebSocketListener() {
|
||||||
private val TAG = "MockWebSocket"
|
private val TAG = "MockWebSocket"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
|
|||||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
@@ -38,7 +38,7 @@ class AttachmentCompressionJobTest {
|
|||||||
StreamUtil.readFully(it)
|
StreamUtil.readFully(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(ApplicationDependencies.getApplication())
|
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(AppDependencies.application)
|
||||||
|
|
||||||
val firstPreUpload = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
|
val firstPreUpload = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
|
||||||
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
|
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
|
||||||
@@ -51,12 +51,12 @@ class AttachmentCompressionJobTest {
|
|||||||
|
|
||||||
val secondJobLatch = CountDownLatch(1)
|
val secondJobLatch = CountDownLatch(1)
|
||||||
val jobThread = Thread {
|
val jobThread = Thread {
|
||||||
firstCompressionJob.setContext(ApplicationDependencies.getApplication())
|
firstCompressionJob.setContext(AppDependencies.application)
|
||||||
firstJobResult = firstCompressionJob.run()
|
firstJobResult = firstCompressionJob.run()
|
||||||
|
|
||||||
secondJobLatch.await()
|
secondJobLatch.await()
|
||||||
|
|
||||||
secondCompressionJob!!.setContext(ApplicationDependencies.getApplication())
|
secondCompressionJob!!.setContext(AppDependencies.application)
|
||||||
secondJobResult = secondCompressionJob!!.run()
|
secondJobResult = secondCompressionJob!!.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.jobs
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import okhttp3.mockwebserver.MockResponse
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.signal.libsignal.usernames.Username
|
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
|
||||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
||||||
import org.thoughtcrime.securesms.testing.Delete
|
|
||||||
import org.thoughtcrime.securesms.testing.Get
|
|
||||||
import org.thoughtcrime.securesms.testing.Put
|
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
|
||||||
import org.thoughtcrime.securesms.testing.failure
|
|
||||||
import org.thoughtcrime.securesms.testing.success
|
|
||||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
|
||||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
|
||||||
import org.whispersystems.util.Base64UrlSafe
|
|
||||||
|
|
||||||
@Suppress("ClassName")
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class RefreshOwnProfileJob__checkUsernameIsInSyncTest {
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val harness = SignalActivityRule()
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
|
||||||
SignalStore.account().usernameOutOfSync = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenNoLocalUsername_whenICheckUsernameIsInSync_thenIExpectNoFailures() {
|
|
||||||
// GIVEN
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Delete("/v1/accounts/username_hash") { MockResponse().success() }
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenLocalUsernameDoesNotMatchServerUsername_whenICheckUsernameIsInSync_thenIExpectRetry() {
|
|
||||||
// GIVEN
|
|
||||||
var didReserve = false
|
|
||||||
var didConfirm = false
|
|
||||||
val username = "hello.32"
|
|
||||||
val serverUsername = "hello.3232"
|
|
||||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Get("/v1/accounts/whoami") { r ->
|
|
||||||
MockResponse().success(
|
|
||||||
WhoAmIResponse().apply {
|
|
||||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(serverUsername))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
|
||||||
didReserve = true
|
|
||||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
|
||||||
},
|
|
||||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
|
||||||
didConfirm = true
|
|
||||||
MockResponse().success()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assertTrue(didReserve)
|
|
||||||
assertTrue(didConfirm)
|
|
||||||
assertFalse(SignalStore.account().usernameOutOfSync)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenLocalAndNoServer_whenICheckUsernameIsInSync_thenIExpectRetry() {
|
|
||||||
// GIVEN
|
|
||||||
var didReserve = false
|
|
||||||
var didConfirm = false
|
|
||||||
val username = "hello.32"
|
|
||||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Get("/v1/accounts/whoami") { r ->
|
|
||||||
MockResponse().success(WhoAmIResponse())
|
|
||||||
},
|
|
||||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
|
||||||
didReserve = true
|
|
||||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
|
||||||
},
|
|
||||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
|
||||||
didConfirm = true
|
|
||||||
MockResponse().success()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assertTrue(didReserve)
|
|
||||||
assertTrue(didConfirm)
|
|
||||||
assertFalse(SignalStore.account().usernameOutOfSync)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenLocalAndServerMatch_whenICheckUsernameIsInSync_thenIExpectNoRetry() {
|
|
||||||
// GIVEN
|
|
||||||
var didReserve = false
|
|
||||||
var didConfirm = false
|
|
||||||
val username = "hello.32"
|
|
||||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Get("/v1/accounts/whoami") { r ->
|
|
||||||
MockResponse().success(
|
|
||||||
WhoAmIResponse().apply {
|
|
||||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
|
||||||
didReserve = true
|
|
||||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
|
||||||
},
|
|
||||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
|
||||||
didConfirm = true
|
|
||||||
MockResponse().success()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assertFalse(didReserve)
|
|
||||||
assertFalse(didConfirm)
|
|
||||||
assertFalse(SignalStore.account().usernameOutOfSync)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun givenMismatchAndReservationFails_whenICheckUsernameIsInSync_thenIExpectNoConfirm() {
|
|
||||||
// GIVEN
|
|
||||||
var didReserve = false
|
|
||||||
var didConfirm = false
|
|
||||||
val username = "hello.32"
|
|
||||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
|
||||||
Get("/v1/accounts/whoami") { r ->
|
|
||||||
MockResponse().success(
|
|
||||||
WhoAmIResponse().apply {
|
|
||||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash("${username}23"))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
|
||||||
didReserve = true
|
|
||||||
MockResponse().failure(418)
|
|
||||||
},
|
|
||||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
|
||||||
didConfirm = true
|
|
||||||
MockResponse().success()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assertTrue(didReserve)
|
|
||||||
assertFalse(didConfirm)
|
|
||||||
assertTrue(SignalStore.account().usernameOutOfSync)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,8 +22,9 @@ import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
|||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.testing.assertIs
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
import org.whispersystems.signalservice.internal.push.Content
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
|
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||||
|
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@@ -38,7 +39,6 @@ class EditMessageSyncProcessorTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
|
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
|
||||||
AttachmentTable.UNIQUE_ID,
|
|
||||||
AttachmentTable.TRANSFER_FILE
|
AttachmentTable.TRANSFER_FILE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -67,16 +67,17 @@ class EditMessageSyncProcessorTest {
|
|||||||
|
|
||||||
val content = MessageContentFuzzer.fuzzTextMessage()
|
val content = MessageContentFuzzer.fuzzTextMessage()
|
||||||
val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id)
|
val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id)
|
||||||
val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
val syncContent = Content.Builder().syncMessage(
|
||||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
SyncMessage.Builder().sent(
|
||||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
SyncMessage.Sent.Builder()
|
||||||
.setDestinationServiceId(metadata.destinationServiceId.toString())
|
.destinationServiceId(metadata.destinationServiceId.toString())
|
||||||
.setTimestamp(originalTimestamp)
|
.timestamp(originalTimestamp)
|
||||||
.setExpirationStartTimestamp(originalTimestamp)
|
.expirationStartTimestamp(originalTimestamp)
|
||||||
.setMessage(content.dataMessage)
|
.message(content.dataMessage)
|
||||||
)
|
.build()
|
||||||
|
).build()
|
||||||
).build()
|
).build()
|
||||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer)
|
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0)
|
||||||
val syncTextMessage = TestMessage(
|
val syncTextMessage = TestMessage(
|
||||||
envelope = MessageContentFuzzer.envelope(originalTimestamp),
|
envelope = MessageContentFuzzer.envelope(originalTimestamp),
|
||||||
content = syncContent,
|
content = syncContent,
|
||||||
@@ -86,18 +87,20 @@ class EditMessageSyncProcessorTest {
|
|||||||
|
|
||||||
val editTimestamp = originalTimestamp + 200
|
val editTimestamp = originalTimestamp + 200
|
||||||
val editedContent = MessageContentFuzzer.fuzzTextMessage()
|
val editedContent = MessageContentFuzzer.fuzzTextMessage()
|
||||||
val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
val editSyncContent = Content.Builder().syncMessage(
|
||||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
SyncMessage.Builder().sent(
|
||||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
SyncMessage.Sent.Builder()
|
||||||
.setDestinationServiceId(metadata.destinationServiceId.toString())
|
.destinationServiceId(metadata.destinationServiceId.toString())
|
||||||
.setTimestamp(editTimestamp)
|
.timestamp(editTimestamp)
|
||||||
.setExpirationStartTimestamp(editTimestamp)
|
.expirationStartTimestamp(editTimestamp)
|
||||||
.setEditMessage(
|
.editMessage(
|
||||||
EditMessage.newBuilder()
|
EditMessage.Builder()
|
||||||
.setDataMessage(editedContent.dataMessage)
|
.dataMessage(editedContent.dataMessage)
|
||||||
.setTargetSentTimestamp(originalTimestamp)
|
.targetSentTimestamp(originalTimestamp)
|
||||||
|
.build()
|
||||||
)
|
)
|
||||||
)
|
.build()
|
||||||
|
).build()
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
val syncEditMessage = TestMessage(
|
val syncEditMessage = TestMessage(
|
||||||
@@ -109,38 +112,38 @@ class EditMessageSyncProcessorTest {
|
|||||||
|
|
||||||
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
|
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
|
||||||
|
|
||||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000)
|
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000)
|
||||||
val originalTextMessage = OutgoingMessage(
|
val originalTextMessage = OutgoingMessage(
|
||||||
threadRecipient = toRecipient,
|
threadRecipient = toRecipient,
|
||||||
sentTimeMillis = originalTimestamp,
|
sentTimeMillis = originalTimestamp,
|
||||||
body = content.dataMessage.body,
|
body = content.dataMessage?.body ?: "",
|
||||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
expiresIn = content.dataMessage?.expireTimer?.seconds?.inWholeMilliseconds ?: 0,
|
||||||
isUrgent = true,
|
isUrgent = true,
|
||||||
isSecure = true,
|
isSecure = true,
|
||||||
bodyRanges = content.dataMessage.bodyRangesList.toBodyRangeList()
|
bodyRanges = content.dataMessage?.bodyRanges.toBodyRangeList()
|
||||||
)
|
)
|
||||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
|
||||||
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
|
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
|
||||||
SignalDatabase.messages.markAsSent(originalMessageId, true)
|
SignalDatabase.messages.markAsSent(originalMessageId, true)
|
||||||
if (content.dataMessage.expireTimer > 0) {
|
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
|
||||||
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
|
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
val editMessage = OutgoingMessage(
|
val editMessage = OutgoingMessage(
|
||||||
threadRecipient = toRecipient,
|
threadRecipient = toRecipient,
|
||||||
sentTimeMillis = editTimestamp,
|
sentTimeMillis = editTimestamp,
|
||||||
body = editedContent.dataMessage.body,
|
body = editedContent.dataMessage?.body ?: "",
|
||||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
expiresIn = content.dataMessage?.expireTimer?.seconds?.inWholeMilliseconds ?: 0,
|
||||||
isUrgent = true,
|
isUrgent = true,
|
||||||
isSecure = true,
|
isSecure = true,
|
||||||
bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(),
|
bodyRanges = editedContent.dataMessage?.bodyRanges.toBodyRangeList(),
|
||||||
messageToEdit = originalMessageId
|
messageToEdit = originalMessageId
|
||||||
)
|
)
|
||||||
|
|
||||||
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
|
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
|
||||||
SignalDatabase.messages.markAsSent(editMessageId, true)
|
SignalDatabase.messages.markAsSent(editMessageId, true)
|
||||||
|
|
||||||
if (content.dataMessage.expireTimer > 0) {
|
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
|
||||||
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
|
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
|
||||||
}
|
}
|
||||||
testResult.collectLocal()
|
testResult.collectLocal()
|
||||||
@@ -167,7 +170,7 @@ class EditMessageSyncProcessorTest {
|
|||||||
|
|
||||||
fun runSync(messages: List<TestMessage>) {
|
fun runSync(messages: List<TestMessage>) {
|
||||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||||
if (content.hasSyncMessage()) {
|
if (content.syncMessage != null) {
|
||||||
processorV2.process(
|
processorV2.process(
|
||||||
envelope,
|
envelope,
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package org.thoughtcrime.securesms.messages
|
package org.thoughtcrime.securesms.messages
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import okio.ByteString.Companion.toByteString
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
|
||||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
|
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
|
||||||
@@ -15,8 +15,8 @@ import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
|||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.testing.assertIs
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||||
|
|
||||||
@Suppress("ClassName")
|
@Suppress("ClassName")
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@@ -41,9 +41,9 @@ class MessageContentProcessor__recipientStatusTest {
|
|||||||
@Test
|
@Test
|
||||||
fun syncGroupSentTextMessageWithRecipientUpdateFollowup() {
|
fun syncGroupSentTextMessageWithRecipientUpdateFollowup() {
|
||||||
val (groupId, masterKey, groupRecipientId) = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), harness.others[0].asMember(), harness.others[1].asMember())
|
val (groupId, masterKey, groupRecipientId) = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), harness.others[0].asMember(), harness.others[1].asMember())
|
||||||
val groupContextV2 = GroupContextV2.newBuilder().setRevision(0).setMasterKey(masterKey.serialize().toProtoByteString()).build()
|
val groupContextV2 = GroupContextV2.Builder().revision(0).masterKey(masterKey.serialize().toByteString()).build()
|
||||||
|
|
||||||
val initialTextMessage = DataMessage.newBuilder().buildWith {
|
val initialTextMessage = DataMessage.Builder().buildWith {
|
||||||
body = MessageContentFuzzer.string()
|
body = MessageContentFuzzer.string()
|
||||||
groupV2 = groupContextV2
|
groupV2 = groupContextV2
|
||||||
timestamp = envelopeTimestamp
|
timestamp = envelopeTimestamp
|
||||||
@@ -52,7 +52,7 @@ class MessageContentProcessor__recipientStatusTest {
|
|||||||
processor.process(
|
processor.process(
|
||||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
|
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
|
||||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
|
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class MessageContentProcessor__recipientStatusTest {
|
|||||||
processor.process(
|
processor.process(
|
||||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true),
|
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true),
|
||||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
|
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.messages
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import org.thoughtcrime.securesms.attachments.Attachment
|
||||||
|
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
||||||
|
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||||
|
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||||
|
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes inserting messages through the "normal" code paths simpler. Mostly focused on incoming messages.
|
||||||
|
*/
|
||||||
|
class MessageHelper(private val harness: SignalActivityRule, var startTime: Long = System.currentTimeMillis()) {
|
||||||
|
|
||||||
|
val alice: RecipientId = harness.others[0]
|
||||||
|
val bob: RecipientId = harness.others[1]
|
||||||
|
val group: GroupTestingUtils.TestGroupInfo = harness.group!!
|
||||||
|
val processor: MessageContentProcessor = MessageContentProcessor(harness.context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val threadIdSlot = slot<Long>()
|
||||||
|
mockkStatic(ThreadUpdateJob::class)
|
||||||
|
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
|
||||||
|
SignalDatabase.threads.update(threadIdSlot.captured, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic(ThreadUpdateJob::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
|
||||||
|
val messageData = MessageData(author = sender, timestamp = startTime)
|
||||||
|
|
||||||
|
processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||||
|
content = MessageContentFuzzer.fuzzTextMessage(
|
||||||
|
sentTimestamp = messageData.timestamp,
|
||||||
|
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null,
|
||||||
|
allowExpireTimeChanges = false
|
||||||
|
),
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||||
|
source = sender,
|
||||||
|
destination = harness.self.id,
|
||||||
|
groupId = if (destination == group.recipientId) group.groupId else null
|
||||||
|
),
|
||||||
|
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: ((OutgoingMessage) -> OutgoingMessage)? = null): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
|
||||||
|
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
||||||
|
val threadRecipient = Recipient.resolved(conversationId)
|
||||||
|
|
||||||
|
val message = OutgoingMessage(
|
||||||
|
threadRecipient = threadRecipient,
|
||||||
|
body = MessageContentFuzzer.string(),
|
||||||
|
sentTimeMillis = messageData.timestamp,
|
||||||
|
isUrgent = true,
|
||||||
|
isSecure = true
|
||||||
|
).let { updateMessage?.invoke(it) ?: it }
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||||
|
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
||||||
|
|
||||||
|
if (successfulSend) {
|
||||||
|
SignalDatabase.messages.markAsSent(messageId, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageData.copy(messageId = messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun outgoingMessage(conversationId: RecipientId = alice, updateMessage: OutgoingMessage.() -> OutgoingMessage): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
|
||||||
|
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
||||||
|
val threadRecipient = Recipient.resolved(conversationId)
|
||||||
|
|
||||||
|
val message = OutgoingMessage(
|
||||||
|
threadRecipient = threadRecipient,
|
||||||
|
sentTimeMillis = messageData.timestamp,
|
||||||
|
isUrgent = true,
|
||||||
|
isSecure = true
|
||||||
|
).apply { updateMessage() }
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||||
|
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
||||||
|
|
||||||
|
return messageData.copy(messageId = messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
|
||||||
|
val uri: Uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
||||||
|
|
||||||
|
val attachment: UriAttachment = UriAttachmentBuilder.build(
|
||||||
|
id = Random.nextLong(),
|
||||||
|
uri = uri,
|
||||||
|
contentType = MediaUtil.IMAGE_JPEG,
|
||||||
|
transformProperties = AttachmentTable.TransformProperties(),
|
||||||
|
uuid = uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
return attachment
|
||||||
|
}
|
||||||
|
|
||||||
|
fun outgoingGroupChange(): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
|
||||||
|
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
||||||
|
val groupRecipient = Recipient.resolved(group.recipientId)
|
||||||
|
val decryptedGroupV2Context = DecryptedGroupV2Context(
|
||||||
|
context = group.groupV2Context,
|
||||||
|
groupState = SignalDatabase.groups.getGroup(group.groupId).get().requireV2GroupProperties().decryptedGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
val updateDescription = GV2UpdateDescription.Builder()
|
||||||
|
.gv2ChangeDescription(decryptedGroupV2Context)
|
||||||
|
.groupChangeUpdate(GroupsV2UpdateMessageConverter.translateDecryptedChange(SignalStore.account.getServiceIds(), decryptedGroupV2Context))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, startTime)
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
|
||||||
|
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null)
|
||||||
|
SignalDatabase.messages.markAsSent(messageId, true)
|
||||||
|
|
||||||
|
return messageData.copy(messageId = messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
|
||||||
|
val messageData = MessageData(author = sender, timestamp = startTime)
|
||||||
|
|
||||||
|
processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||||
|
content = MessageContentFuzzer.fuzzStickerMediaMessage(
|
||||||
|
sentTimestamp = messageData.timestamp,
|
||||||
|
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||||
|
),
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||||
|
source = sender,
|
||||||
|
destination = harness.self.id,
|
||||||
|
groupId = if (destination == group.recipientId) group.groupId else null
|
||||||
|
),
|
||||||
|
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
|
||||||
|
val messageData = MessageData(author = sender, timestamp = startTime)
|
||||||
|
|
||||||
|
processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||||
|
content = MessageContentFuzzer.editTextMessage(
|
||||||
|
targetTimestamp = targetTimestamp,
|
||||||
|
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
|
||||||
|
sentTimestamp = messageData.timestamp,
|
||||||
|
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||||
|
).dataMessage!!
|
||||||
|
),
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||||
|
source = sender,
|
||||||
|
destination = harness.self.id,
|
||||||
|
groupId = if (destination == group.recipientId) group.groupId else null
|
||||||
|
),
|
||||||
|
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
val messageData = MessageData(timestamp = startTime)
|
||||||
|
|
||||||
|
processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||||
|
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||||
|
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeleteForMeMessage(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
val messageData = MessageData(timestamp = startTime)
|
||||||
|
|
||||||
|
processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||||
|
content = MessageContentFuzzer.syncDeleteForMeMessage(deletes.toList()),
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||||
|
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeleteForMeConversation(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
val messageData = MessageData(timestamp = startTime)
|
||||||
|
|
||||||
|
processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||||
|
content = MessageContentFuzzer.syncDeleteForMeConversation(deletes.toList()),
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||||
|
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeleteForMeLocalOnlyConversation(vararg conversations: RecipientId): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
val messageData = MessageData(timestamp = startTime)
|
||||||
|
|
||||||
|
processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||||
|
content = MessageContentFuzzer.syncDeleteForMeLocalOnlyConversation(conversations.toList()),
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||||
|
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): MessageData {
|
||||||
|
startTime = nextStartTime()
|
||||||
|
val messageData = MessageData(timestamp = startTime)
|
||||||
|
|
||||||
|
processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||||
|
content = MessageContentFuzzer.syncDeleteForMeAttachment(conversationId, message, uuid, digest, plainTextHash),
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||||
|
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next "sentTimestamp" for current + [nextMessageOffset]th message. Useful for early message processing and future message timestamps.
|
||||||
|
*/
|
||||||
|
fun nextStartTime(nextMessageOffset: Int = 1): Long {
|
||||||
|
return startTime + 1000 * nextMessageOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MessageData(
|
||||||
|
val author: RecipientId = RecipientId.UNKNOWN,
|
||||||
|
val serverGuid: UUID = UUID.randomUUID(),
|
||||||
|
val timestamp: Long,
|
||||||
|
val messageId: Long = -1L
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import io.mockk.mockkObject
|
|||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.unmockkStatic
|
import io.mockk.unmockkStatic
|
||||||
import okio.ByteString
|
import okio.ByteString
|
||||||
import okio.ByteString.Companion.toByteString
|
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
@@ -17,7 +16,7 @@ import org.signal.core.util.logging.Log
|
|||||||
import org.signal.libsignal.protocol.ecc.Curve
|
import org.signal.libsignal.protocol.ecc.Curve
|
||||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
|
||||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.testing.AliceClient
|
import org.thoughtcrime.securesms.testing.AliceClient
|
||||||
@@ -26,7 +25,7 @@ import org.thoughtcrime.securesms.testing.Entry
|
|||||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers
|
import org.thoughtcrime.securesms.testing.FakeClientHelpers
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.testing.awaitFor
|
import org.thoughtcrime.securesms.testing.awaitFor
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
import org.whispersystems.signalservice.internal.push.Envelope
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketMessage
|
import org.whispersystems.signalservice.internal.websocket.WebSocketMessage
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
@@ -56,8 +55,8 @@ class MessageProcessingPerformanceTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockkStatic(UnidentifiedAccessUtil::class)
|
mockkStatic(SealedSenderAccessUtil::class)
|
||||||
every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
|
every { SealedSenderAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
|
||||||
|
|
||||||
mockkObject(MessageContentProcessor)
|
mockkObject(MessageContentProcessor)
|
||||||
every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application)
|
every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application)
|
||||||
@@ -65,7 +64,7 @@ class MessageProcessingPerformanceTest {
|
|||||||
|
|
||||||
@After
|
@After
|
||||||
fun after() {
|
fun after() {
|
||||||
unmockkStatic(UnidentifiedAccessUtil::class)
|
unmockkStatic(SealedSenderAccessUtil::class)
|
||||||
unmockkStatic(MessageContentProcessor::class)
|
unmockkStatic(MessageContentProcessor::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ class MessageProcessingPerformanceTest {
|
|||||||
val messageCount = 100
|
val messageCount = 100
|
||||||
val envelopes = generateInboundEnvelopes(bobClient, messageCount)
|
val envelopes = generateInboundEnvelopes(bobClient, messageCount)
|
||||||
val firstTimestamp = envelopes.first().timestamp
|
val firstTimestamp = envelopes.first().timestamp
|
||||||
val lastTimestamp = envelopes.last().timestamp
|
val lastTimestamp = envelopes.last().timestamp ?: 0
|
||||||
|
|
||||||
// Inject the envelopes into the websocket
|
// Inject the envelopes into the websocket
|
||||||
Thread {
|
Thread {
|
||||||
@@ -190,7 +189,7 @@ class MessageProcessingPerformanceTest {
|
|||||||
path = "/api/v1/message",
|
path = "/api/v1/message",
|
||||||
id = Random(System.currentTimeMillis()).nextLong(),
|
id = Random(System.currentTimeMillis()).nextLong(),
|
||||||
headers = listOf("X-Signal-Timestamp: ${this.timestamp}"),
|
headers = listOf("X-Signal-Timestamp: ${this.timestamp}"),
|
||||||
body = this.toByteArray().toByteString()
|
body = this.encodeByteString()
|
||||||
)
|
)
|
||||||
).encodeByteString()
|
).encodeByteString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.messages
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SyncMessageProcessorTest_readSyncs {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule(createGroup = true)
|
||||||
|
|
||||||
|
private lateinit var messageHelper: MessageHelper
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
messageHelper = MessageHelper(harness)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
messageHelper.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSynchronizeReadMessage() {
|
||||||
|
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||||
|
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||||
|
threadRecord.unreadCount assertIs 2
|
||||||
|
|
||||||
|
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
|
||||||
|
|
||||||
|
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||||
|
threadRecord.unreadCount assertIs 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSynchronizeReadMessageMissingTimestamp() {
|
||||||
|
messageHelper.incomingText().timestamp
|
||||||
|
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||||
|
threadRecord.unreadCount assertIs 2
|
||||||
|
|
||||||
|
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp)
|
||||||
|
|
||||||
|
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||||
|
threadRecord.unreadCount assertIs 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSynchronizeReadWithEdits() {
|
||||||
|
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||||
|
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
|
||||||
|
|
||||||
|
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
|
||||||
|
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
|
||||||
|
|
||||||
|
val message2Timestamp = messageHelper.incomingMedia().timestamp
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||||
|
threadRecord.unreadCount assertIs 2
|
||||||
|
|
||||||
|
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
|
||||||
|
|
||||||
|
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||||
|
threadRecord.unreadCount assertIs 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSynchronizeReadWithEditsInGroup() {
|
||||||
|
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||||
|
|
||||||
|
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
|
||||||
|
|
||||||
|
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||||
|
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||||
|
|
||||||
|
val message2Timestamp = messageHelper.incomingMedia(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||||
|
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||||
|
threadRecord.unreadCount assertIs 2
|
||||||
|
|
||||||
|
messageHelper.syncReadMessage(messageHelper.bob to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
|
||||||
|
|
||||||
|
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||||
|
threadRecord.unreadCount assertIs 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,703 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.messages
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.hamcrest.Matchers.greaterThan
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.update
|
||||||
|
import org.signal.core.util.withinTransaction
|
||||||
|
import org.thoughtcrime.securesms.attachments.Attachment
|
||||||
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
import org.thoughtcrime.securesms.database.CallTable
|
||||||
|
import org.thoughtcrime.securesms.database.MessageTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.testing.MessageContentFuzzer.DeleteForMeSync
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.testing.assert
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||||
|
import org.thoughtcrime.securesms.util.IdentityUtil
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SyncMessageProcessorTest_synchronizeDeleteForMe {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "SyncDeleteForMeTest"
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule(createGroup = true)
|
||||||
|
|
||||||
|
private lateinit var messageHelper: MessageHelper
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
messageHelper = MessageHelper(harness)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
messageHelper.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleMessageDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||||
|
messageHelper.incomingText()
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 2
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeMessage(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleOutgoingMessageDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val message1Timestamp = messageHelper.outgoingText().timestamp
|
||||||
|
messageHelper.incomingText()
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 2
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeMessage(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.alice, harness.self.id to message1Timestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleGroupMessageDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||||
|
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
|
||||||
|
messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId)
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||||
|
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 3
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeMessage(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun multipleGroupMessageDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||||
|
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
|
||||||
|
val message3Timestamp = messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||||
|
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 3
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeMessage(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp, messageHelper.bob to message3Timestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun allMessagesDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||||
|
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 2
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeMessage(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 0
|
||||||
|
|
||||||
|
val threadRecord = SignalDatabase.threads.getThreadRecord(threadId)
|
||||||
|
threadRecord assertIs null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun earlyMessagesDelete() {
|
||||||
|
// GIVEN
|
||||||
|
messageHelper.incomingText().timestamp
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 1
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val nextTextMessageTimestamp = messageHelper.nextStartTime(2)
|
||||||
|
messageHelper.syncDeleteForMeMessage(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to nextTextMessageTimestamp)
|
||||||
|
)
|
||||||
|
messageHelper.incomingText()
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||||
|
messageCount assertIs 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun multipleConversationMessagesDelete() {
|
||||||
|
// GIVEN
|
||||||
|
messageHelper.incomingText(sender = messageHelper.alice)
|
||||||
|
val aliceMessage2 = messageHelper.incomingText(sender = messageHelper.alice).timestamp
|
||||||
|
|
||||||
|
messageHelper.incomingText(sender = messageHelper.bob)
|
||||||
|
val bobMessage2 = messageHelper.incomingText(sender = messageHelper.bob).timestamp
|
||||||
|
|
||||||
|
val aliceThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
var aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
|
||||||
|
aliceMessageCount assertIs 2
|
||||||
|
|
||||||
|
val bobThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.bob)!!
|
||||||
|
var bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
|
||||||
|
bobMessageCount assertIs 2
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeMessage(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to aliceMessage2),
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.bob, messageHelper.bob to bobMessage2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
|
||||||
|
aliceMessageCount assertIs 1
|
||||||
|
|
||||||
|
bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
|
||||||
|
bobMessageCount assertIs 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleConversationDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||||
|
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||||
|
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeConversation(
|
||||||
|
DeleteForMeSync(
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
|
||||||
|
isFullDelete = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||||
|
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleConversationNoRecentsFoundDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||||
|
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||||
|
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val randomFutureMessages = (1..5).map {
|
||||||
|
messageHelper.alice to messageHelper.nextStartTime(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
messageHelper.syncDeleteForMeConversation(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, isFullDelete = true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||||
|
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
|
||||||
|
|
||||||
|
harness.inMemoryLogger.flush()
|
||||||
|
harness.inMemoryLogger.entries().filter { it.message?.contains("Unable to find most recent received at timestamp") == true }.size assertIs 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleConversationNoRecentsFoundNonExpiringRecentsFoundDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||||
|
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||||
|
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val nonExpiringMessages = messages.takeLast(5).map { it.recipientId to it.timetamp }
|
||||||
|
|
||||||
|
val randomFutureMessages = (1..5).map {
|
||||||
|
messageHelper.alice to messageHelper.nextStartTime(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
messageHelper.syncDeleteForMeConversation(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, nonExpiringMessages, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||||
|
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||||
|
|
||||||
|
harness.inMemoryLogger.flush()
|
||||||
|
harness.inMemoryLogger.entries().filter { it.message?.contains("Using backup non-expiring messages") == true }.size assertIs 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun localOnlyRemainingAfterConversationDeleteWithFullDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||||
|
|
||||||
|
Log.v(TAG, "Adding normal messages")
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||||
|
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val alice = Recipient.resolved(messageHelper.alice)
|
||||||
|
Log.v(TAG, "Adding identity message")
|
||||||
|
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
|
||||||
|
Log.v(TAG, "Adding profile message")
|
||||||
|
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||||
|
Log.v(TAG, "Adding call message")
|
||||||
|
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
Log.v(TAG, "Processing sync message")
|
||||||
|
messageHelper.syncDeleteForMeConversation(
|
||||||
|
DeleteForMeSync(
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
|
||||||
|
isFullDelete = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||||
|
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun localOnlyRemainingAfterConversationDeleteWithoutFullDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||||
|
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||||
|
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val alice = Recipient.resolved(messageHelper.alice)
|
||||||
|
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
|
||||||
|
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||||
|
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeConversation(
|
||||||
|
DeleteForMeSync(
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
|
||||||
|
isFullDelete = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 3
|
||||||
|
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun groupConversationDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||||
|
|
||||||
|
for (i in 0 until 50) {
|
||||||
|
messages += when (i % 3) {
|
||||||
|
1 -> MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp)
|
||||||
|
2 -> MessageTable.SyncMessageId(messageHelper.bob, messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp)
|
||||||
|
else -> MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(messageHelper.group.recipientId).timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeConversation(
|
||||||
|
DeleteForMeSync(
|
||||||
|
conversationId = messageHelper.group.recipientId,
|
||||||
|
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
|
||||||
|
isFullDelete = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||||
|
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun multipleConversationDelete() {
|
||||||
|
// GIVEN
|
||||||
|
val allMessages = mapOf<RecipientId, MutableList<MessageTable.SyncMessageId>>(
|
||||||
|
messageHelper.alice to mutableListOf(),
|
||||||
|
messageHelper.bob to mutableListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
allMessages.forEach { (conversation, messages) ->
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
messages += MessageTable.SyncMessageId(conversation, messageHelper.incomingText(sender = conversation).timestamp)
|
||||||
|
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(conversationId = conversation).timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadIds = allMessages.keys.map { SignalDatabase.threads.getThreadIdFor(it)!! }
|
||||||
|
threadIds.forEach { SignalDatabase.messages.getMessageCountForThread(it) assertIs 20 }
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeConversation(
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.alice, allMessages[messageHelper.alice]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true),
|
||||||
|
DeleteForMeSync(conversationId = messageHelper.bob, allMessages[messageHelper.bob]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
threadIds.forEach {
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(it) assertIs 0
|
||||||
|
SignalDatabase.threads.getThreadRecord(it) assertIs null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleLocalOnlyConversation() {
|
||||||
|
// GIVEN
|
||||||
|
val alice = Recipient.resolved(messageHelper.alice)
|
||||||
|
|
||||||
|
// Insert placeholder message to prevent early thread update deletes
|
||||||
|
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
|
||||||
|
|
||||||
|
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
|
||||||
|
|
||||||
|
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
|
||||||
|
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
|
||||||
|
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||||
|
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
|
||||||
|
|
||||||
|
// Cleanup and confirm setup
|
||||||
|
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assert greaterThan(0)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
|
||||||
|
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("counts are consistent for some reason")
|
||||||
|
@Test
|
||||||
|
fun multipleLocalOnlyConversation() {
|
||||||
|
// GIVEN
|
||||||
|
val alice = Recipient.resolved(messageHelper.alice)
|
||||||
|
|
||||||
|
// Insert placeholder messages in group and alice thread to prevent early thread update deletes
|
||||||
|
val groupPlaceholderMessage = messageHelper.outgoingText(conversationId = messageHelper.group.recipientId).messageId
|
||||||
|
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
|
||||||
|
|
||||||
|
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
|
||||||
|
val groupThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.group.recipientId, isGroup = true)
|
||||||
|
|
||||||
|
// Identity changes
|
||||||
|
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
|
||||||
|
IdentityUtil.markIdentityVerified(harness.context, alice, false, true)
|
||||||
|
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
|
||||||
|
IdentityUtil.markIdentityVerified(harness.context, alice, false, false)
|
||||||
|
|
||||||
|
IdentityUtil.markIdentityUpdate(harness.context, alice.id)
|
||||||
|
|
||||||
|
// Calls
|
||||||
|
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
|
||||||
|
SignalDatabase.calls.insertOneToOneCall(2, System.currentTimeMillis(), alice.id, CallTable.Type.VIDEO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED)
|
||||||
|
SignalDatabase.calls.insertOneToOneCall(3, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED_NOTIFICATION_PROFILE)
|
||||||
|
|
||||||
|
SignalDatabase.calls.insertAcceptedGroupCall(4, messageHelper.group.recipientId, CallTable.Direction.INCOMING, System.currentTimeMillis())
|
||||||
|
SignalDatabase.calls.insertDeclinedGroupCall(5, messageHelper.group.recipientId, System.currentTimeMillis())
|
||||||
|
|
||||||
|
// Detected changes
|
||||||
|
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||||
|
SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, null, "username.42")
|
||||||
|
SignalDatabase.messages.insertNumberChangeMessages(alice.id)
|
||||||
|
SignalDatabase.messages.insertSmsExportMessage(alice.id, SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!)
|
||||||
|
SignalDatabase.messages.insertSessionSwitchoverEvent(alice.id, aliceThreadId, SessionSwitchoverEvent())
|
||||||
|
|
||||||
|
// Sent failed
|
||||||
|
SignalDatabase.messages.markAsSending(messageHelper.outgoingText().messageId)
|
||||||
|
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
|
||||||
|
messageHelper.outgoingText().let {
|
||||||
|
SignalDatabase.messages.markAsSending(it.messageId)
|
||||||
|
SignalDatabase.messages.markAsRateLimited(it.messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group change
|
||||||
|
messageHelper.outgoingGroupChange()
|
||||||
|
|
||||||
|
// Cleanup and confirm setup
|
||||||
|
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
|
||||||
|
SignalDatabase.messages.deleteMessage(messageId = groupPlaceholderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
|
||||||
|
|
||||||
|
SignalDatabase.rawDatabase.withinTransaction {
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 16
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice, messageHelper.group.recipientId)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
|
||||||
|
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
|
||||||
|
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 0
|
||||||
|
SignalDatabase.threads.getThreadRecord(groupThreadId) assertIs null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleLocalOnlyConversationHasAddressable() {
|
||||||
|
// GIVEN
|
||||||
|
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||||
|
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||||
|
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||||
|
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
|
||||||
|
|
||||||
|
harness.inMemoryLogger.flush()
|
||||||
|
harness.inMemoryLogger.entries().filter { it.message?.contains("Thread is not local only") == true }.size assertIs 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleAttachmentDeletes() {
|
||||||
|
// GIVEN
|
||||||
|
val message1 = messageHelper.outgoingText { message ->
|
||||||
|
message.copy(
|
||||||
|
attachments = listOf(
|
||||||
|
messageHelper.outgoingAttachment(byteArrayOf(1, 2, 3)),
|
||||||
|
messageHelper.outgoingAttachment(byteArrayOf(2, 3, 4), null),
|
||||||
|
messageHelper.outgoingAttachment(byteArrayOf(5, 6, 7), null),
|
||||||
|
messageHelper.outgoingAttachment(byteArrayOf(10, 11, 12))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||||
|
attachments assertIsSize 4
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||||
|
|
||||||
|
// Has all three
|
||||||
|
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||||
|
id = attachments[0].attachmentId,
|
||||||
|
attachment = attachments[0].copy(digest = byteArrayOf(attachments[0].attachmentId.id.toByte())),
|
||||||
|
uploadTimestamp = message1.timestamp + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Missing uuid and digest
|
||||||
|
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||||
|
id = attachments[1].attachmentId,
|
||||||
|
attachment = attachments[1],
|
||||||
|
uploadTimestamp = message1.timestamp + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Missing uuid and plain text
|
||||||
|
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||||
|
id = attachments[2].attachmentId,
|
||||||
|
attachment = attachments[2].copy(digest = byteArrayOf(attachments[2].attachmentId.id.toByte())),
|
||||||
|
uploadTimestamp = message1.timestamp + 1
|
||||||
|
)
|
||||||
|
SignalDatabase.rawDatabase.update(AttachmentTable.TABLE_NAME).values(AttachmentTable.DATA_HASH_END to null).where("${AttachmentTable.ID} = ?", attachments[2].attachmentId).run()
|
||||||
|
|
||||||
|
// Different has all three
|
||||||
|
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||||
|
id = attachments[3].attachmentId,
|
||||||
|
attachment = attachments[3].copy(digest = byteArrayOf(attachments[3].attachmentId.id.toByte())),
|
||||||
|
uploadTimestamp = message1.timestamp + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
messageHelper.syncDeleteForMeAttachment(
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
message = message1.author to message1.timestamp,
|
||||||
|
attachments[0].uuid,
|
||||||
|
attachments[0].remoteDigest,
|
||||||
|
attachments[0].dataHash
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||||
|
var updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||||
|
updatedAttachments assertIsSize 3
|
||||||
|
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[0].attachmentId }
|
||||||
|
|
||||||
|
messageHelper.syncDeleteForMeAttachment(
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
message = message1.author to message1.timestamp,
|
||||||
|
attachments[1].uuid,
|
||||||
|
attachments[1].remoteDigest,
|
||||||
|
attachments[1].dataHash
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||||
|
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||||
|
updatedAttachments assertIsSize 2
|
||||||
|
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[1].attachmentId }
|
||||||
|
|
||||||
|
messageHelper.syncDeleteForMeAttachment(
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
message = message1.author to message1.timestamp,
|
||||||
|
attachments[2].uuid,
|
||||||
|
attachments[2].remoteDigest,
|
||||||
|
attachments[2].dataHash
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||||
|
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||||
|
updatedAttachments assertIsSize 1
|
||||||
|
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[2].attachmentId }
|
||||||
|
|
||||||
|
messageHelper.syncDeleteForMeAttachment(
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
message = message1.author to message1.timestamp,
|
||||||
|
attachments[3].uuid,
|
||||||
|
attachments[3].remoteDigest,
|
||||||
|
attachments[3].dataHash
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||||
|
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||||
|
updatedAttachments assertIsSize 0
|
||||||
|
|
||||||
|
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DatabaseAttachment.copy(
|
||||||
|
uuid: UUID? = this.uuid,
|
||||||
|
digest: ByteArray? = this.remoteDigest
|
||||||
|
): Attachment {
|
||||||
|
return DatabaseAttachment(
|
||||||
|
attachmentId = this.attachmentId,
|
||||||
|
mmsId = this.mmsId,
|
||||||
|
hasData = this.hasData,
|
||||||
|
hasThumbnail = false,
|
||||||
|
hasArchiveThumbnail = false,
|
||||||
|
contentType = this.contentType,
|
||||||
|
transferProgress = this.transferState,
|
||||||
|
size = this.size,
|
||||||
|
fileName = this.fileName,
|
||||||
|
cdn = this.cdn,
|
||||||
|
location = this.remoteLocation,
|
||||||
|
key = this.remoteKey,
|
||||||
|
digest = digest,
|
||||||
|
incrementalDigest = this.incrementalDigest,
|
||||||
|
incrementalMacChunkSize = this.incrementalMacChunkSize,
|
||||||
|
fastPreflightId = this.fastPreflightId,
|
||||||
|
voiceNote = this.voiceNote,
|
||||||
|
borderless = this.borderless,
|
||||||
|
videoGif = this.videoGif,
|
||||||
|
width = this.width,
|
||||||
|
height = this.height,
|
||||||
|
quote = this.quote,
|
||||||
|
caption = this.caption,
|
||||||
|
stickerLocator = this.stickerLocator,
|
||||||
|
blurHash = this.blurHash,
|
||||||
|
audioHash = this.audioHash,
|
||||||
|
transformProperties = this.transformProperties,
|
||||||
|
displayOrder = this.displayOrder,
|
||||||
|
uploadTimestamp = this.uploadTimestamp,
|
||||||
|
dataHash = this.dataHash,
|
||||||
|
archiveCdn = this.archiveCdn,
|
||||||
|
archiveThumbnailCdn = this.archiveThumbnailCdn,
|
||||||
|
archiveMediaName = this.archiveMediaName,
|
||||||
|
archiveMediaId = this.archiveMediaId,
|
||||||
|
thumbnailRestoreState = this.thumbnailRestoreState,
|
||||||
|
uuid = uuid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms.messages
|
package org.thoughtcrime.securesms.messages
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
import org.whispersystems.signalservice.internal.push.Content
|
||||||
|
import org.whispersystems.signalservice.internal.push.Envelope
|
||||||
|
|
||||||
data class TestMessage(
|
data class TestMessage(
|
||||||
val envelope: SignalServiceProtos.Envelope,
|
val envelope: Envelope,
|
||||||
val content: SignalServiceProtos.Content,
|
val content: Content,
|
||||||
val metadata: EnvelopeMetadata,
|
val metadata: EnvelopeMetadata,
|
||||||
val serverDeliveredTimestamp: Long
|
val serverDeliveredTimestamp: Long
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import org.signal.core.util.logging.Log
|
|||||||
import org.thoughtcrime.securesms.testing.LogPredicate
|
import org.thoughtcrime.securesms.testing.LogPredicate
|
||||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
import org.whispersystems.signalservice.internal.push.Content
|
||||||
|
import org.whispersystems.signalservice.internal.push.Envelope
|
||||||
|
|
||||||
class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(context) {
|
class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(context) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -19,9 +20,9 @@ class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(
|
|||||||
fun endTag(timestamp: Long) = "$timestamp end"
|
fun endTag(timestamp: Long) = "$timestamp end"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
|
override fun process(envelope: Envelope, content: Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
|
||||||
Log.d(TAG, startTag(envelope.timestamp))
|
Log.d(TAG, startTag(envelope.timestamp!!))
|
||||||
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent, localMetric)
|
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent, localMetric)
|
||||||
Log.d(TAG, endTag(envelope.timestamp))
|
Log.d(TAG, endTag(envelope.timestamp!!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package org.thoughtcrime.securesms.migrations
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.count
|
||||||
|
import org.signal.core.util.readToSingleInt
|
||||||
|
import org.signal.donations.PaymentSourceType
|
||||||
|
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SubscriberIdMigrationJobTest {
|
||||||
|
|
||||||
|
private val testSubject = SubscriberIdMigrationJob()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenNoSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectNoDatabaseEntries() {
|
||||||
|
testSubject.run()
|
||||||
|
|
||||||
|
val actual = SignalDatabase.inAppPaymentSubscribers.readableDatabase.count()
|
||||||
|
.from(InAppPaymentSubscriberTable.TABLE_NAME)
|
||||||
|
.run()
|
||||||
|
.readToSingleInt()
|
||||||
|
|
||||||
|
actual assertIs 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenUSDSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectASingleEntry() {
|
||||||
|
val subscriberId = SubscriberId.generate()
|
||||||
|
SignalStore.inAppPayments.setSubscriberCurrency(Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION)
|
||||||
|
SignalStore.inAppPayments.setSubscriber("USD", subscriberId)
|
||||||
|
SignalStore.inAppPayments.setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
|
||||||
|
SignalStore.inAppPayments.shouldCancelSubscriptionBeforeNextSubscribeAttempt = true
|
||||||
|
|
||||||
|
testSubject.run()
|
||||||
|
|
||||||
|
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD", InAppPaymentSubscriberRecord.Type.DONATION)
|
||||||
|
|
||||||
|
actual.assertIsNotNull()
|
||||||
|
actual!!.subscriberId.bytes assertIs subscriberId.bytes
|
||||||
|
actual.paymentMethodType assertIs InAppPaymentData.PaymentMethodType.PAYPAL
|
||||||
|
actual.requiresCancel assertIs true
|
||||||
|
actual.currency assertIs Currency.getInstance("USD")
|
||||||
|
actual.type assertIs InAppPaymentSubscriberRecord.Type.DONATION
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.testing.SignalActivityRule
|
|||||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||||
import org.thoughtcrime.securesms.testing.assertIsNull
|
import org.thoughtcrime.securesms.testing.assertIsNull
|
||||||
import org.thoughtcrime.securesms.testing.success
|
import org.thoughtcrime.securesms.testing.success
|
||||||
|
import org.whispersystems.signalservice.api.util.Usernames
|
||||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@@ -56,27 +57,10 @@ class UsernameEditFragmentTest {
|
|||||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testUsernameCreationInRegistration() {
|
|
||||||
val scenario = createScenario(true)
|
|
||||||
|
|
||||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
|
||||||
|
|
||||||
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
|
|
||||||
noViewFoundException.assertIsNull()
|
|
||||||
val toolbar = view as Toolbar
|
|
||||||
|
|
||||||
toolbar.navigationIcon.assertIsNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
|
|
||||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore("Flakey espresso test.")
|
@Ignore("Flakey espresso test.")
|
||||||
@Test
|
@Test
|
||||||
fun testUsernameCreationOutsideOfRegistration() {
|
fun testUsernameCreationOutsideOfRegistration() {
|
||||||
val scenario = createScenario()
|
val scenario = createScenario(UsernameEditMode.NORMAL)
|
||||||
|
|
||||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||||
|
|
||||||
@@ -96,7 +80,7 @@ class UsernameEditFragmentTest {
|
|||||||
fun testNicknameUpdateHappyPath() {
|
fun testNicknameUpdateHappyPath() {
|
||||||
val nickname = "Spiderman"
|
val nickname = "Spiderman"
|
||||||
val discriminator = "4578"
|
val discriminator = "4578"
|
||||||
val username = "$nickname${UsernameState.DELIMITER}$discriminator"
|
val username = "$nickname${Usernames.DELIMITER}$discriminator"
|
||||||
|
|
||||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||||
Put("/v1/accounts/username/reserved") {
|
Put("/v1/accounts/username/reserved") {
|
||||||
@@ -107,7 +91,7 @@ class UsernameEditFragmentTest {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val scenario = createScenario(isInRegistration = true)
|
val scenario = createScenario(UsernameEditMode.NORMAL)
|
||||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||||
|
|
||||||
onView(withId(R.id.username_text)).perform(typeText(nickname))
|
onView(withId(R.id.username_text)).perform(typeText(nickname))
|
||||||
@@ -131,8 +115,8 @@ class UsernameEditFragmentTest {
|
|||||||
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
|
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> {
|
private fun createScenario(mode: UsernameEditMode = UsernameEditMode.NORMAL): FragmentScenario<UsernameEditFragment> {
|
||||||
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle()
|
val fragmentArgs = UsernameEditFragmentArgs.Builder().setMode(mode).build().toBundle()
|
||||||
return launchFragmentInContainer(
|
return launchFragmentInContainer(
|
||||||
fragmentArgs = fragmentArgs,
|
fragmentArgs = fragmentArgs,
|
||||||
themeResId = R.style.Signal_DayNight_NoActionBar
|
themeResId = R.style.Signal_DayNight_NoActionBar
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ class SafetyNumberBottomSheetRepositoryTest {
|
|||||||
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
|
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
|
||||||
testSubscriber.request(1)
|
testSubscriber.request(1)
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
testSubscriber.awaitCount(3)
|
testSubscriber.awaitCount(2)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
testSubscriber.assertValueAt(2) { map ->
|
testSubscriber.assertValueAt(1) { map ->
|
||||||
assertMatch(
|
assertMatch(
|
||||||
map,
|
map,
|
||||||
mapOf(
|
mapOf(
|
||||||
@@ -116,10 +116,10 @@ class SafetyNumberBottomSheetRepositoryTest {
|
|||||||
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
|
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
|
||||||
testSubscriber.request(1)
|
testSubscriber.request(1)
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
testSubscriber.awaitCount(3)
|
testSubscriber.awaitCount(2)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
testSubscriber.assertValueAt(2) { map ->
|
testSubscriber.assertValueAt(1) { map ->
|
||||||
assertMatch(map, mapOf())
|
assertMatch(map, mapOf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import org.junit.Assert.assertNotEquals
|
|||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.update
|
import org.signal.core.util.update
|
||||||
import org.thoughtcrime.securesms.database.RecipientTable
|
import org.thoughtcrime.securesms.database.RecipientTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.Base64
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||||
@@ -26,14 +24,13 @@ class ContactRecordProcessorTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
SignalStore.account().setE164(E164_SELF)
|
SignalStore.account.setE164(E164_SELF)
|
||||||
SignalStore.account().setAci(ACI_SELF)
|
SignalStore.account.setAci(ACI_SELF)
|
||||||
SignalStore.account().setPni(PNI_SELF)
|
SignalStore.account.setPni(PNI_SELF)
|
||||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun process_splitContact_normalSplit() {
|
fun process_splitContact_normalSplit_twoRecords() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||||
setStorageId(originalId, STORAGE_ID_A)
|
setStorageId(originalId, STORAGE_ID_A)
|
||||||
@@ -69,6 +66,35 @@ class ContactRecordProcessorTest {
|
|||||||
assertNotEquals(byAci, byE164)
|
assertNotEquals(byAci, byE164)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun process_splitContact_normalSplit_oneRecord() {
|
||||||
|
// GIVEN
|
||||||
|
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||||
|
setStorageId(originalId, STORAGE_ID_A)
|
||||||
|
|
||||||
|
val remote = buildRecord(
|
||||||
|
STORAGE_ID_B,
|
||||||
|
ContactRecord(
|
||||||
|
aci = ACI_A.toString(),
|
||||||
|
unregisteredAtTimestamp = 100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val subject = ContactRecordProcessor()
|
||||||
|
subject.process(listOf(remote), StorageSyncHelper.KEY_GENERATOR)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||||
|
|
||||||
|
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||||
|
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||||
|
|
||||||
|
assertEquals(originalId, byAci)
|
||||||
|
assertEquals(byE164, byPni)
|
||||||
|
assertNotEquals(byAci, byE164)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun process_splitContact_doNotSplitIfAciRecordIsRegistered() {
|
fun process_splitContact_doNotSplitIfAciRecordIsRegistered() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
@@ -113,7 +139,7 @@ class ContactRecordProcessorTest {
|
|||||||
private fun setStorageId(recipientId: RecipientId, storageId: StorageId) {
|
private fun setStorageId(recipientId: RecipientId, storageId: StorageId) {
|
||||||
SignalDatabase.rawDatabase
|
SignalDatabase.rawDatabase
|
||||||
.update(RecipientTable.TABLE_NAME)
|
.update(RecipientTable.TABLE_NAME)
|
||||||
.values(RecipientTable.STORAGE_SERVICE_ID to Base64.encodeBytes(storageId.raw))
|
.values(RecipientTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(storageId.raw))
|
||||||
.where("${RecipientTable.ID} = ?", recipientId)
|
.where("${RecipientTable.ID} = ?", recipientId)
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ package org.thoughtcrime.securesms.testing
|
|||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
|
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
import org.whispersystems.signalservice.internal.push.Envelope
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Welcome to Alice's Client.
|
* Welcome to Alice's Client.
|
||||||
@@ -30,17 +29,17 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
|
|||||||
uuid = serviceId.rawUuid,
|
uuid = serviceId.rawUuid,
|
||||||
e164 = e164,
|
e164 = e164,
|
||||||
deviceId = 1,
|
deviceId = 1,
|
||||||
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,
|
identityKey = SignalStore.account.aciIdentityKey.publicKey.publicKey,
|
||||||
expires = 31337
|
expires = 31337
|
||||||
)
|
)
|
||||||
|
|
||||||
fun process(envelope: Envelope, serverDeliveredTimestamp: Long) {
|
fun process(envelope: Envelope, serverDeliveredTimestamp: Long) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val bufferedStore = BufferedProtocolStore.create()
|
val bufferedStore = BufferedProtocolStore.create()
|
||||||
ApplicationDependencies.getIncomingMessageObserver()
|
AppDependencies.incomingMessageObserver
|
||||||
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
|
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
|
||||||
?.mapNotNull { it.run() }
|
?.mapNotNull { it.run() }
|
||||||
?.forEach { ApplicationDependencies.getJobManager().add(it) }
|
?.forEach { it.enqueue() }
|
||||||
|
|
||||||
bufferedStore.flushToDisk()
|
bufferedStore.flushToDisk()
|
||||||
val end = System.currentTimeMillis()
|
val end = System.currentTimeMillis()
|
||||||
@@ -48,9 +47,9 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun encrypt(now: Long, destination: Recipient): Envelope {
|
fun encrypt(now: Long, destination: Recipient): Envelope {
|
||||||
return ApplicationDependencies.getSignalServiceMessageSender().getEncryptedMessage(
|
return AppDependencies.signalServiceMessageSender.getEncryptedMessage(
|
||||||
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
|
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
|
||||||
FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate),
|
FakeClientHelpers.getSealedSenderAccess(ProfileKey(destination.profileKey), aliceSenderCertificate),
|
||||||
1,
|
1,
|
||||||
FakeClientHelpers.encryptedTextMessage(now),
|
FakeClientHelpers.encryptedTextMessage(now),
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
|||||||
import org.signal.libsignal.protocol.util.KeyHelper
|
import org.signal.libsignal.protocol.util.KeyHelper
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
|
||||||
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
|
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable
|
import org.thoughtcrime.securesms.database.SignedPreKeyTable
|
||||||
@@ -25,17 +25,15 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|||||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
|
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
|
||||||
import org.whispersystems.signalservice.api.SignalSessionLock
|
import org.whispersystems.signalservice.api.SignalSessionLock
|
||||||
|
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
|
||||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
|
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
|
||||||
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder
|
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId
|
import org.whispersystems.signalservice.api.push.DistributionId
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
import org.whispersystems.signalservice.internal.push.Envelope
|
||||||
import java.util.Optional
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
import kotlin.UnsupportedOperationException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Welcome to Bob's Client.
|
* Welcome to Bob's Client.
|
||||||
@@ -61,7 +59,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Inspired by SignalServiceMessageSender#getEncryptedMessage */
|
/** Inspired by SignalServiceMessageSender#getEncryptedMessage */
|
||||||
fun encrypt(now: Long): SignalServiceProtos.Envelope {
|
fun encrypt(now: Long): Envelope {
|
||||||
val envelopeContent = FakeClientHelpers.encryptedTextMessage(now)
|
val envelopeContent = FakeClientHelpers.encryptedTextMessage(now)
|
||||||
|
|
||||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
|
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
|
||||||
@@ -72,16 +70,16 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent)
|
return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent)
|
||||||
.toEnvelope(envelopeContent.content.get().dataMessage.timestamp, getAliceServiceId())
|
.toEnvelope(envelopeContent.content.get().dataMessage!!.timestamp!!, getAliceServiceId())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decrypt(envelope: SignalServiceProtos.Envelope, serverDeliveredTimestamp: Long) {
|
fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) {
|
||||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
|
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, SealedSenderAccessUtil.getCertificateValidator())
|
||||||
cipher.decrypt(envelope, serverDeliveredTimestamp)
|
cipher.decrypt(envelope, serverDeliveredTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAliceServiceId(): ServiceId {
|
private fun getAliceServiceId(): ServiceId {
|
||||||
return SignalStore.account().requireAci()
|
return SignalStore.account.requireAci()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAlicePreKeyBundle(): PreKeyBundle {
|
private fun getAlicePreKeyBundle(): PreKeyBundle {
|
||||||
@@ -104,7 +102,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
|||||||
val selfSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), selfSignedPreKeyId)!!
|
val selfSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), selfSignedPreKeyId)!!
|
||||||
|
|
||||||
return PreKeyBundle(
|
return PreKeyBundle(
|
||||||
SignalStore.account().registrationId,
|
SignalStore.account.registrationId,
|
||||||
1,
|
1,
|
||||||
selfPreKeyId,
|
selfPreKeyId,
|
||||||
selfPreKeyRecord.keyPair.publicKey,
|
selfPreKeyRecord.keyPair.publicKey,
|
||||||
@@ -116,19 +114,19 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getAliceProtocolAddress(): SignalProtocolAddress {
|
private fun getAliceProtocolAddress(): SignalProtocolAddress {
|
||||||
return SignalProtocolAddress(SignalStore.account().requireAci().toString(), 1)
|
return SignalProtocolAddress(SignalStore.account.requireAci().toString(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAlicePublicKey(): IdentityKey {
|
private fun getAlicePublicKey(): IdentityKey {
|
||||||
return SignalStore.account().aciIdentityKey.publicKey
|
return SignalStore.account.aciIdentityKey.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAliceProfileKey(): ProfileKey {
|
private fun getAliceProfileKey(): ProfileKey {
|
||||||
return ProfileKeyUtil.getSelfProfileKey()
|
return ProfileKeyUtil.getSelfProfileKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAliceUnidentifiedAccess(): Optional<UnidentifiedAccess> {
|
private fun getAliceUnidentifiedAccess(): SealedSenderAccess? {
|
||||||
return FakeClientHelpers.getTargetUnidentifiedAccess(profileKey, getAliceProfileKey(), senderCertificate)
|
return FakeClientHelpers.getSealedSenderAccess(getAliceProfileKey(), senderCertificate)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {
|
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {
|
||||||
@@ -140,10 +138,12 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
|||||||
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
|
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
|
||||||
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
|
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
|
||||||
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): Boolean = false
|
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): Boolean = false
|
||||||
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) { aliceSessionRecord = record }
|
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) {
|
||||||
|
aliceSessionRecord = record
|
||||||
|
}
|
||||||
override fun getSubDeviceSessions(name: String?): List<Int> = emptyList()
|
override fun getSubDeviceSessions(name: String?): List<Int> = emptyList()
|
||||||
override fun containsSession(address: SignalProtocolAddress?): Boolean = aliceSessionRecord != null
|
override fun containsSession(address: SignalProtocolAddress?): Boolean = aliceSessionRecord != null
|
||||||
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account().aciIdentityKey.publicKey
|
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account.aciIdentityKey.publicKey
|
||||||
override fun loadPreKey(preKeyId: Int): PreKeyRecord = throw UnsupportedOperationException()
|
override fun loadPreKey(preKeyId: Int): PreKeyRecord = throw UnsupportedOperationException()
|
||||||
override fun storePreKey(preKeyId: Int, record: PreKeyRecord?) = throw UnsupportedOperationException()
|
override fun storePreKey(preKeyId: Int, record: PreKeyRecord?) = throw UnsupportedOperationException()
|
||||||
override fun containsPreKey(preKeyId: Int): Boolean = throw UnsupportedOperationException()
|
override fun containsPreKey(preKeyId: Int): Boolean = throw UnsupportedOperationException()
|
||||||
@@ -166,7 +166,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
|||||||
override fun storeSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?, record: SenderKeyRecord?) = throw UnsupportedOperationException()
|
override fun storeSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?, record: SenderKeyRecord?) = throw UnsupportedOperationException()
|
||||||
override fun loadSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?): SenderKeyRecord = throw UnsupportedOperationException()
|
override fun loadSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?): SenderKeyRecord = throw UnsupportedOperationException()
|
||||||
override fun archiveSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
|
override fun archiveSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
|
||||||
override fun getAllAddressesWithActiveSessions(addressNames: MutableList<String>?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
override fun getAllAddressesWithActiveSessions(addressNames: MutableList<String>?): MutableMap<SignalProtocolAddress, SessionRecord> = throw UnsupportedOperationException()
|
||||||
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
||||||
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||||
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.testing
|
package org.thoughtcrime.securesms.testing
|
||||||
|
|
||||||
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.libsignal.internal.Native
|
import org.signal.libsignal.internal.Native
|
||||||
import org.signal.libsignal.internal.NativeHandleGuard
|
import org.signal.libsignal.internal.NativeHandleGuard
|
||||||
import org.signal.libsignal.metadata.certificate.CertificateValidator
|
import org.signal.libsignal.metadata.certificate.CertificateValidator
|
||||||
@@ -9,16 +11,16 @@ import org.signal.libsignal.protocol.ecc.Curve
|
|||||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||||
import org.whispersystems.signalservice.api.crypto.ContentHint
|
import org.whispersystems.signalservice.api.crypto.ContentHint
|
||||||
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
|
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
|
||||||
|
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
|
import org.whispersystems.signalservice.internal.push.Content
|
||||||
|
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||||
|
import org.whispersystems.signalservice.internal.push.Envelope
|
||||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
|
||||||
import org.whispersystems.util.Base64
|
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -44,17 +46,16 @@ object FakeClientHelpers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTargetUnidentifiedAccess(myProfileKey: ProfileKey, theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): Optional<UnidentifiedAccess> {
|
fun getSealedSenderAccess(theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): SealedSenderAccess? {
|
||||||
val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey)
|
val themUnidentifiedAccessKey = UnidentifiedAccess(UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey), senderCertificate.serialized, false)
|
||||||
val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
|
|
||||||
|
|
||||||
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized, false), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized, false)).targetUnidentifiedAccess
|
return SealedSenderAccess.forIndividual(themUnidentifiedAccessKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {
|
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {
|
||||||
val content = SignalServiceProtos.Content.newBuilder().apply {
|
val content = Content.Builder().apply {
|
||||||
setDataMessage(
|
dataMessage(
|
||||||
SignalServiceProtos.DataMessage.newBuilder().apply {
|
DataMessage.Builder().buildWith {
|
||||||
body = message
|
body = message
|
||||||
timestamp = now
|
timestamp = now
|
||||||
}
|
}
|
||||||
@@ -64,16 +65,16 @@ object FakeClientHelpers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope {
|
fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope {
|
||||||
return Envelope.newBuilder()
|
return Envelope.Builder()
|
||||||
.setType(Envelope.Type.valueOf(this.type))
|
.type(Envelope.Type.fromValue(this.type))
|
||||||
.setSourceDevice(1)
|
.sourceDevice(1)
|
||||||
.setTimestamp(timestamp)
|
.timestamp(timestamp)
|
||||||
.setServerTimestamp(timestamp + 1)
|
.serverTimestamp(timestamp + 1)
|
||||||
.setDestinationServiceId(destination.toString())
|
.destinationServiceId(destination.toString())
|
||||||
.setServerGuid(UUID.randomUUID().toString())
|
.serverGuid(UUID.randomUUID().toString())
|
||||||
.setContent(Base64.decode(this.content).toProtoByteString())
|
.content(Base64.decode(this.content).toByteString())
|
||||||
.setUrgent(true)
|
.urgent(true)
|
||||||
.setStory(false)
|
.story(false)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.testing
|
package org.thoughtcrime.securesms.testing
|
||||||
|
|
||||||
|
import okio.ByteString.Companion.toByteString
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||||
import org.signal.storageservice.protos.groups.Member
|
import org.signal.storageservice.protos.groups.Member
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||||
@@ -9,6 +10,7 @@ import org.thoughtcrime.securesms.groups.GroupId
|
|||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
|
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,22 +18,22 @@ import kotlin.random.Random
|
|||||||
*/
|
*/
|
||||||
object GroupTestingUtils {
|
object GroupTestingUtils {
|
||||||
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
|
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
|
||||||
return DecryptedMember.newBuilder()
|
return DecryptedMember.Builder()
|
||||||
.setAciBytes(aci.toByteString())
|
.aciBytes(aci.toByteString())
|
||||||
.setJoinedAtRevision(revision)
|
.joinedAtRevision(revision)
|
||||||
.setRole(role)
|
.role(role)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertGroup(revision: Int = 0, vararg members: DecryptedMember): TestGroupInfo {
|
fun insertGroup(revision: Int = 0, vararg members: DecryptedMember): TestGroupInfo {
|
||||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
val decryptedGroupState = DecryptedGroup.Builder()
|
||||||
.addAllMembers(members.toList())
|
.members(members.toList())
|
||||||
.setRevision(revision)
|
.revision(revision)
|
||||||
.setTitle(MessageContentFuzzer.string())
|
.title(MessageContentFuzzer.string())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!!
|
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState, null)!!
|
||||||
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
|
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
|
||||||
SignalDatabase.recipients.setProfileSharing(groupRecipientId, true)
|
SignalDatabase.recipients.setProfileSharing(groupRecipientId, true)
|
||||||
|
|
||||||
@@ -46,5 +48,8 @@ object GroupTestingUtils {
|
|||||||
return member(aci = requireAci())
|
return member(aci = requireAci())
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
|
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) {
|
||||||
|
val groupV2Context: GroupContextV2
|
||||||
|
get() = GroupContextV2(masterKey = masterKey.serialize().toByteString(), revision = 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.thoughtcrime.securesms.testing
|
package org.thoughtcrime.securesms.testing
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
import okio.ByteString
|
||||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import org.signal.core.util.Base64
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||||
import org.thoughtcrime.securesms.messages.TestMessage
|
import org.thoughtcrime.securesms.messages.TestMessage
|
||||||
@@ -9,13 +11,14 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
|
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
|
import org.whispersystems.signalservice.internal.push.Content
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
import org.whispersystems.signalservice.internal.push.Envelope
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||||
|
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.random.nextInt
|
import kotlin.random.nextInt
|
||||||
@@ -34,22 +37,22 @@ object MessageContentFuzzer {
|
|||||||
/**
|
/**
|
||||||
* Create an [Envelope].
|
* Create an [Envelope].
|
||||||
*/
|
*/
|
||||||
fun envelope(timestamp: Long): Envelope {
|
fun envelope(timestamp: Long, serverGuid: UUID = UUID.randomUUID()): Envelope {
|
||||||
return Envelope.newBuilder()
|
return Envelope.Builder()
|
||||||
.setTimestamp(timestamp)
|
.timestamp(timestamp)
|
||||||
.setServerTimestamp(timestamp + 5)
|
.serverTimestamp(timestamp + 5)
|
||||||
.setServerGuidBytes(UuidUtil.toByteString(UUID.randomUUID()))
|
.serverGuid(serverGuid.toString())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create metadata to match an [Envelope].
|
* Create metadata to match an [Envelope].
|
||||||
*/
|
*/
|
||||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
fun envelopeMetadata(source: RecipientId, destination: RecipientId, sourceDeviceId: Int = 1, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
||||||
return EnvelopeMetadata(
|
return EnvelopeMetadata(
|
||||||
sourceServiceId = Recipient.resolved(source).requireServiceId(),
|
sourceServiceId = Recipient.resolved(source).requireServiceId(),
|
||||||
sourceE164 = null,
|
sourceE164 = null,
|
||||||
sourceDeviceId = 1,
|
sourceDeviceId = sourceDeviceId,
|
||||||
sealedSender = true,
|
sealedSender = true,
|
||||||
groupId = groupId?.decodedId,
|
groupId = groupId?.decodedId,
|
||||||
destinationServiceId = Recipient.resolved(destination).requireServiceId()
|
destinationServiceId = Recipient.resolved(destination).requireServiceId()
|
||||||
@@ -61,21 +64,24 @@ object MessageContentFuzzer {
|
|||||||
* - An expire timer value
|
* - An expire timer value
|
||||||
* - Bold style body ranges
|
* - Bold style body ranges
|
||||||
*/
|
*/
|
||||||
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
|
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null, allowExpireTimeChanges: Boolean = true): Content {
|
||||||
return Content.newBuilder()
|
return Content.Builder()
|
||||||
.setDataMessage(
|
.dataMessage(
|
||||||
DataMessage.newBuilder().buildWith {
|
DataMessage.Builder().buildWith {
|
||||||
|
timestamp = sentTimestamp
|
||||||
body = string()
|
body = string()
|
||||||
if (random.nextBoolean()) {
|
if (allowExpireTimeChanges && random.nextBoolean()) {
|
||||||
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
||||||
}
|
}
|
||||||
if (random.nextBoolean()) {
|
if (random.nextBoolean()) {
|
||||||
addBodyRanges(
|
bodyRanges(
|
||||||
SignalServiceProtos.BodyRange.newBuilder().buildWith {
|
listOf(
|
||||||
start = 0
|
BodyRange.Builder().buildWith {
|
||||||
length = 1
|
start = 0
|
||||||
style = SignalServiceProtos.BodyRange.Style.BOLD
|
length = 1
|
||||||
}
|
style = BodyRange.Style.BOLD
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (groupContextV2 != null) {
|
if (groupContextV2 != null) {
|
||||||
@@ -86,6 +92,20 @@ object MessageContentFuzzer {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an edit message.
|
||||||
|
*/
|
||||||
|
fun editTextMessage(targetTimestamp: Long, editedDataMessage: DataMessage): Content {
|
||||||
|
return Content.Builder()
|
||||||
|
.editMessage(
|
||||||
|
EditMessage.Builder().buildWith {
|
||||||
|
targetSentTimestamp = targetTimestamp
|
||||||
|
dataMessage = editedDataMessage
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a sync sent text message for the given [DataMessage].
|
* Create a sync sent text message for the given [DataMessage].
|
||||||
*/
|
*/
|
||||||
@@ -95,16 +115,16 @@ object MessageContentFuzzer {
|
|||||||
recipientUpdate: Boolean = false
|
recipientUpdate: Boolean = false
|
||||||
): Content {
|
): Content {
|
||||||
return Content
|
return Content
|
||||||
.newBuilder()
|
.Builder()
|
||||||
.setSyncMessage(
|
.syncMessage(
|
||||||
SyncMessage.newBuilder().buildWith {
|
SyncMessage.Builder().buildWith {
|
||||||
sent = SyncMessage.Sent.newBuilder().buildWith {
|
sent = SyncMessage.Sent.Builder().buildWith {
|
||||||
timestamp = textMessage.timestamp
|
timestamp = textMessage.timestamp
|
||||||
message = textMessage
|
message = textMessage
|
||||||
isRecipientUpdate = recipientUpdate
|
isRecipientUpdate = recipientUpdate
|
||||||
addAllUnidentifiedStatus(
|
unidentifiedStatus(
|
||||||
deliveredTo.map {
|
deliveredTo.map {
|
||||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder().buildWith {
|
SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder().buildWith {
|
||||||
destinationServiceId = Recipient.resolved(it).requireServiceId().toString()
|
destinationServiceId = Recipient.resolved(it).requireServiceId().toString()
|
||||||
unidentified = true
|
unidentified = true
|
||||||
}
|
}
|
||||||
@@ -115,6 +135,139 @@ object MessageContentFuzzer {
|
|||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a sync reads message for the given [RecipientId] and message timestamp pairings.
|
||||||
|
*/
|
||||||
|
fun syncReadsMessage(timestamps: List<Pair<RecipientId, Long>>): Content {
|
||||||
|
return Content
|
||||||
|
.Builder()
|
||||||
|
.syncMessage(
|
||||||
|
SyncMessage.Builder().buildWith {
|
||||||
|
read = timestamps.map { (senderId, timestamp) ->
|
||||||
|
SyncMessage.Read.Builder().buildWith {
|
||||||
|
this.senderAci = Recipient.resolved(senderId).requireAci().toString()
|
||||||
|
this.timestamp = timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeleteForMeMessage(allDeletes: List<DeleteForMeSync>): Content {
|
||||||
|
return Content
|
||||||
|
.Builder()
|
||||||
|
.syncMessage(
|
||||||
|
SyncMessage(
|
||||||
|
deleteForMe = SyncMessage.DeleteForMe(
|
||||||
|
messageDeletes = allDeletes.map { (conversationId, conversationDeletes) ->
|
||||||
|
val conversation = Recipient.resolved(conversationId)
|
||||||
|
SyncMessage.DeleteForMe.MessageDeletes(
|
||||||
|
conversation = if (conversation.isGroup) {
|
||||||
|
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||||
|
} else {
|
||||||
|
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||||
|
},
|
||||||
|
|
||||||
|
messages = conversationDeletes.map { (author, timestamp) ->
|
||||||
|
SyncMessage.DeleteForMe.AddressableMessage(
|
||||||
|
authorServiceId = Recipient.resolved(author).requireAci().toString(),
|
||||||
|
sentTimestamp = timestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeleteForMeConversation(allDeletes: List<DeleteForMeSync>): Content {
|
||||||
|
return Content
|
||||||
|
.Builder()
|
||||||
|
.syncMessage(
|
||||||
|
SyncMessage(
|
||||||
|
deleteForMe = SyncMessage.DeleteForMe(
|
||||||
|
conversationDeletes = allDeletes.map { delete ->
|
||||||
|
val conversation = Recipient.resolved(delete.conversationId)
|
||||||
|
SyncMessage.DeleteForMe.ConversationDelete(
|
||||||
|
conversation = if (conversation.isGroup) {
|
||||||
|
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||||
|
} else {
|
||||||
|
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||||
|
},
|
||||||
|
|
||||||
|
mostRecentMessages = delete.messages.map { (author, timestamp) ->
|
||||||
|
SyncMessage.DeleteForMe.AddressableMessage(
|
||||||
|
authorServiceId = Recipient.resolved(author).requireAci().toString(),
|
||||||
|
sentTimestamp = timestamp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
mostRecentNonExpiringMessages = delete.nonExpiringMessages.map { (author, timestamp) ->
|
||||||
|
SyncMessage.DeleteForMe.AddressableMessage(
|
||||||
|
authorServiceId = Recipient.resolved(author).requireAci().toString(),
|
||||||
|
sentTimestamp = timestamp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
isFullDelete = delete.isFullDelete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeleteForMeLocalOnlyConversation(conversations: List<RecipientId>): Content {
|
||||||
|
return Content
|
||||||
|
.Builder()
|
||||||
|
.syncMessage(
|
||||||
|
SyncMessage(
|
||||||
|
deleteForMe = SyncMessage.DeleteForMe(
|
||||||
|
localOnlyConversationDeletes = conversations.map { conversationId ->
|
||||||
|
val conversation = Recipient.resolved(conversationId)
|
||||||
|
SyncMessage.DeleteForMe.LocalOnlyConversationDelete(
|
||||||
|
conversation = if (conversation.isGroup) {
|
||||||
|
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||||
|
} else {
|
||||||
|
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): Content {
|
||||||
|
val conversation = Recipient.resolved(conversationId)
|
||||||
|
|
||||||
|
return Content
|
||||||
|
.Builder()
|
||||||
|
.syncMessage(
|
||||||
|
SyncMessage(
|
||||||
|
deleteForMe = SyncMessage.DeleteForMe(
|
||||||
|
attachmentDeletes = listOf(
|
||||||
|
SyncMessage.DeleteForMe.AttachmentDelete(
|
||||||
|
conversation = if (conversation.isGroup) {
|
||||||
|
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||||
|
} else {
|
||||||
|
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||||
|
},
|
||||||
|
targetMessage = SyncMessage.DeleteForMe.AddressableMessage(
|
||||||
|
authorServiceId = Recipient.resolved(message.first).requireAci().toString(),
|
||||||
|
sentTimestamp = message.second
|
||||||
|
),
|
||||||
|
uuid = uuid?.let { UuidUtil.toByteString(it) },
|
||||||
|
fallbackDigest = digest?.toByteString(),
|
||||||
|
fallbackPlaintextHash = plainTextHash?.let { Base64.decodeOrNull(it)?.toByteString() }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a random media message that may be:
|
* Create a random media message that may be:
|
||||||
* - A text body
|
* - A text body
|
||||||
@@ -123,9 +276,9 @@ object MessageContentFuzzer {
|
|||||||
* - A message with 0-2 attachment pointers and may contain a text body
|
* - A message with 0-2 attachment pointers and may contain a text body
|
||||||
*/
|
*/
|
||||||
fun fuzzMediaMessageWithBody(quoteAble: List<TestMessage> = emptyList()): Content {
|
fun fuzzMediaMessageWithBody(quoteAble: List<TestMessage> = emptyList()): Content {
|
||||||
return Content.newBuilder()
|
return Content.Builder()
|
||||||
.setDataMessage(
|
.dataMessage(
|
||||||
DataMessage.newBuilder().buildWith {
|
DataMessage.Builder().buildWith {
|
||||||
if (random.nextBoolean()) {
|
if (random.nextBoolean()) {
|
||||||
body = string()
|
body = string()
|
||||||
}
|
}
|
||||||
@@ -133,28 +286,28 @@ object MessageContentFuzzer {
|
|||||||
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
|
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
|
||||||
body = string()
|
body = string()
|
||||||
val quoted = quoteAble.random(random)
|
val quoted = quoteAble.random(random)
|
||||||
quote = DataMessage.Quote.newBuilder().buildWith {
|
quote = DataMessage.Quote.Builder().buildWith {
|
||||||
id = quoted.envelope.timestamp
|
id = quoted.envelope.timestamp
|
||||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||||
text = quoted.content.dataMessage.body
|
text = quoted.content.dataMessage?.body
|
||||||
addAllAttachments(quoted.content.dataMessage.attachmentsList)
|
attachments(quoted.content.dataMessage?.attachments ?: emptyList())
|
||||||
addAllBodyRanges(quoted.content.dataMessage.bodyRangesList)
|
bodyRanges(quoted.content.dataMessage?.bodyRanges ?: emptyList())
|
||||||
type = DataMessage.Quote.Type.NORMAL
|
type = DataMessage.Quote.Type.NORMAL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) {
|
if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) {
|
||||||
val quoted = quoteAble.random(random)
|
val quoted = quoteAble.random(random)
|
||||||
quote = DataMessage.Quote.newBuilder().buildWith {
|
quote = DataMessage.Quote.Builder().buildWith {
|
||||||
id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp)
|
id = random.nextLong(quoted.envelope.timestamp!! - 1000000, quoted.envelope.timestamp!!)
|
||||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||||
text = quoted.content.dataMessage.body
|
text = quoted.content.dataMessage?.body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (random.nextFloat() < 0.25) {
|
if (random.nextFloat() < 0.25) {
|
||||||
val total = random.nextInt(1, 2)
|
val total = random.nextInt(1, 2)
|
||||||
(0..total).forEach { _ -> addAttachments(attachmentPointer()) }
|
attachments((0..total).map { attachmentPointer() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -166,12 +319,12 @@ object MessageContentFuzzer {
|
|||||||
* - A reaction to a prior message
|
* - A reaction to a prior message
|
||||||
*/
|
*/
|
||||||
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
|
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||||
return Content.newBuilder()
|
return Content.Builder()
|
||||||
.setDataMessage(
|
.dataMessage(
|
||||||
DataMessage.newBuilder().buildWith {
|
DataMessage.Builder().buildWith {
|
||||||
if (random.nextFloat() < 0.25) {
|
if (random.nextFloat() < 0.25) {
|
||||||
val reactTo = previousMessages.random(random)
|
val reactTo = previousMessages.random(random)
|
||||||
reaction = DataMessage.Reaction.newBuilder().buildWith {
|
reaction = DataMessage.Reaction.Builder().buildWith {
|
||||||
emoji = emojis.random(random)
|
emoji = emojis.random(random)
|
||||||
remove = false
|
remove = false
|
||||||
targetAuthorAci = reactTo.metadata.sourceServiceId.toString()
|
targetAuthorAci = reactTo.metadata.sourceServiceId.toString()
|
||||||
@@ -183,22 +336,21 @@ object MessageContentFuzzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a random media message that can never contain a text body. It may be:
|
* Create a random media message that contains a sticker.
|
||||||
* - A sticker
|
|
||||||
*/
|
*/
|
||||||
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
|
fun fuzzStickerMediaMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
|
||||||
return Content.newBuilder()
|
return Content.Builder()
|
||||||
.setDataMessage(
|
.dataMessage(
|
||||||
DataMessage.newBuilder().buildWith {
|
DataMessage.Builder().buildWith {
|
||||||
if (random.nextFloat() < 0.9) {
|
timestamp = sentTimestamp
|
||||||
sticker = DataMessage.Sticker.newBuilder().buildWith {
|
sticker = DataMessage.Sticker.Builder().buildWith {
|
||||||
packId = byteString(length = 24)
|
packId = byteString(length = 24)
|
||||||
packKey = byteString(length = 128)
|
packKey = byteString(length = 128)
|
||||||
stickerId = random.nextInt()
|
stickerId = random.nextInt()
|
||||||
data = attachmentPointer()
|
data_ = attachmentPointer()
|
||||||
emoji = emojis.random(random)
|
emoji = emojis.random(random)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
groupV2 = groupContextV2
|
||||||
}
|
}
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
@@ -223,14 +375,14 @@ object MessageContentFuzzer {
|
|||||||
* Generate a random [ByteString].
|
* Generate a random [ByteString].
|
||||||
*/
|
*/
|
||||||
fun byteString(length: Int = 512): ByteString {
|
fun byteString(length: Int = 512): ByteString {
|
||||||
return random.nextBytes(length).toProtoByteString()
|
return random.nextBytes(length).toByteString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random [AttachmentPointer].
|
* Generate a random [AttachmentPointer].
|
||||||
*/
|
*/
|
||||||
fun attachmentPointer(): AttachmentPointer {
|
fun attachmentPointer(): AttachmentPointer {
|
||||||
return AttachmentPointer.newBuilder().run {
|
return AttachmentPointer.Builder().run {
|
||||||
cdnKey = string()
|
cdnKey = string()
|
||||||
contentType = mediaTypes.random(random)
|
contentType = mediaTypes.random(random)
|
||||||
key = byteString()
|
key = byteString()
|
||||||
@@ -244,7 +396,7 @@ object MessageContentFuzzer {
|
|||||||
caption = string(allowNullString = true)
|
caption = string(allowNullString = true)
|
||||||
blurHash = string()
|
blurHash = string()
|
||||||
uploadTimestamp = random.nextLong()
|
uploadTimestamp = random.nextLong()
|
||||||
cdnNumber = 1
|
cdnNumber = 2
|
||||||
|
|
||||||
build()
|
build()
|
||||||
}
|
}
|
||||||
@@ -256,4 +408,14 @@ object MessageContentFuzzer {
|
|||||||
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
|
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
|
||||||
return envelopeTimestamp + 10
|
return envelopeTimestamp + 10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class DeleteForMeSync(
|
||||||
|
val conversationId: RecipientId,
|
||||||
|
val messages: List<Pair<RecipientId, Long>>,
|
||||||
|
val nonExpiringMessages: List<Pair<RecipientId, Long>> = emptyList(),
|
||||||
|
val isFullDelete: Boolean = true,
|
||||||
|
val attachments: List<Pair<Long, AttachmentTable.SyncAttachmentId>> = emptyList()
|
||||||
|
) {
|
||||||
|
constructor(conversationId: RecipientId, vararg messages: Pair<RecipientId, Long>) : this(conversationId, messages.toList())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms.testing
|
package org.thoughtcrime.securesms.testing
|
||||||
|
|
||||||
import org.mockito.kotlin.anyOrNull
|
|
||||||
import org.mockito.kotlin.doReturn
|
|
||||||
import org.mockito.kotlin.stub
|
|
||||||
import org.signal.core.util.Hex
|
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||||
import org.signal.libsignal.protocol.ecc.Curve
|
import org.signal.libsignal.protocol.ecc.Curve
|
||||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||||
import org.signal.libsignal.protocol.util.KeyHelper
|
import org.signal.libsignal.protocol.util.KeyHelper
|
||||||
import org.signal.libsignal.protocol.util.Medium
|
import org.signal.libsignal.protocol.util.Medium
|
||||||
import org.signal.libsignal.svr2.PinHash
|
|
||||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.test.BuildConfig
|
|
||||||
import org.whispersystems.signalservice.api.KeyBackupService
|
|
||||||
import org.whispersystems.signalservice.api.SvrPinData
|
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||||
@@ -41,7 +31,7 @@ object MockProvider {
|
|||||||
|
|
||||||
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
|
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
|
||||||
svr1Credentials = AuthCredentials.create("username", "password")
|
svr1Credentials = AuthCredentials.create("username", "password")
|
||||||
svr2Credentials = null
|
svr2Credentials = AuthCredentials.create("username", "password")
|
||||||
}
|
}
|
||||||
|
|
||||||
val primaryOnlyDeviceList = DeviceInfoList().apply {
|
val primaryOnlyDeviceList = DeviceInfoList().apply {
|
||||||
@@ -78,19 +68,7 @@ object MockProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mockGetRegistrationLockStringFlow() {
|
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account.aciIdentityKey, deviceId: Int): PreKeyResponse {
|
||||||
val session: KeyBackupService.RestoreSession = object : KeyBackupService.RestoreSession {
|
|
||||||
override fun hashSalt(): ByteArray = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8")
|
|
||||||
override fun restorePin(hashedPin: PinHash?): SvrPinData = SvrPinData(MasterKey.createNew(SecureRandom()), null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val kbsService = ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE)
|
|
||||||
kbsService.stub {
|
|
||||||
on { newRegistrationSession(anyOrNull(), anyOrNull()) } doReturn session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
|
|
||||||
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
|
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
|
||||||
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())
|
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())
|
||||||
|
|
||||||
|
|||||||
@@ -55,5 +55,5 @@ inline fun <reified T> RecordedRequest.parsedRequestBody(): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultRequestPredicate(verb: String, path: String, predicate: RequestPredicate = { true }): RequestPredicate = { request ->
|
private fun defaultRequestPredicate(verb: String, path: String, predicate: RequestPredicate = { true }): RequestPredicate = { request ->
|
||||||
request.method == verb && request.path.startsWith("/$path") && predicate(request)
|
request.method == verb && request.path?.startsWith("/$path") == true && predicate(request)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
|||||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||||
import org.thoughtcrime.securesms.database.IdentityTable
|
import org.thoughtcrime.securesms.database.IdentityTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
@@ -47,7 +47,7 @@ import java.util.UUID
|
|||||||
*/
|
*/
|
||||||
class SignalActivityRule(private val othersCount: Int = 4, private val createGroup: Boolean = false) : ExternalResource() {
|
class SignalActivityRule(private val othersCount: Int = 4, private val createGroup: Boolean = false) : ExternalResource() {
|
||||||
|
|
||||||
val application: Application = ApplicationDependencies.getApplication()
|
val application: Application = AppDependencies.application
|
||||||
|
|
||||||
lateinit var context: Context
|
lateinit var context: Context
|
||||||
private set
|
private set
|
||||||
@@ -90,8 +90,8 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
|||||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||||
|
|
||||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||||
|
|
||||||
val registrationRepository = RegistrationRepository(application)
|
val registrationRepository = RegistrationRepository(application)
|
||||||
|
|
||||||
@@ -111,19 +111,19 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
|||||||
verifyAccountResponse = VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
verifyAccountResponse = VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
||||||
masterKey = null,
|
masterKey = null,
|
||||||
pin = null,
|
pin = null,
|
||||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
|
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
|
||||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
|
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
|
||||||
),
|
),
|
||||||
false
|
false
|
||||||
).blockingGet()
|
).blockingGet()
|
||||||
|
|
||||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||||
|
|
||||||
SignalStore.svr().optOut()
|
SignalStore.svr.optOut()
|
||||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||||
|
|
||||||
SignalStore.settings().isMessageNotificationsEnabled = false
|
SignalStore.settings.isMessageNotificationsEnabled = false
|
||||||
|
|
||||||
return Recipient.self()
|
return Recipient.self()
|
||||||
}
|
}
|
||||||
@@ -141,11 +141,11 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
|||||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
|
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false))
|
||||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
||||||
others += recipientId
|
others += recipientId
|
||||||
othersKeys += otherIdentity
|
othersKeys += otherIdentity
|
||||||
}
|
}
|
||||||
@@ -158,14 +158,14 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
|
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
|
||||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
|
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIdentity(recipient: Recipient): IdentityKey {
|
fun getIdentity(recipient: Recipient): IdentityKey {
|
||||||
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
|
return AppDependencies.protocolStore.aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
|
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
|
||||||
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
|
AppDependencies.protocolStore.aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.testing
|
|||||||
|
|
||||||
import org.junit.rules.TestWatcher
|
import org.junit.rules.TestWatcher
|
||||||
import org.junit.runner.Description
|
import org.junit.runner.Description
|
||||||
|
import org.signal.core.util.deleteAll
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadTable
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
@@ -24,8 +26,8 @@ class SignalDatabaseRule(
|
|||||||
override fun starting(description: Description?) {
|
override fun starting(description: Description?) {
|
||||||
deleteAllThreads()
|
deleteAllThreads()
|
||||||
|
|
||||||
SignalStore.account().setAci(localAci)
|
SignalStore.account.setAci(localAci)
|
||||||
SignalStore.account().setPni(localPni)
|
SignalStore.account.setPni(localPni)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finished(description: Description?) {
|
override fun finished(description: Description?) {
|
||||||
@@ -34,7 +36,8 @@ class SignalDatabaseRule(
|
|||||||
|
|
||||||
private fun deleteAllThreads() {
|
private fun deleteAllThreads() {
|
||||||
if (deleteAllThreadsOnEachRun) {
|
if (deleteAllThreadsOnEachRun) {
|
||||||
SignalDatabase.threads.clearForTests()
|
SignalDatabase.threads.deleteAllConversations()
|
||||||
|
SignalDatabase.rawDatabase.deleteAll(ThreadTable.TABLE_NAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.testing
|
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
|
||||||
import org.whispersystems.signalservice.internal.serialize.protos.AddressProto
|
|
||||||
import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto
|
|
||||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class TestProtos private constructor() {
|
|
||||||
fun address(
|
|
||||||
uuid: UUID = UUID.randomUUID()
|
|
||||||
): AddressProto.Builder {
|
|
||||||
return AddressProto.newBuilder()
|
|
||||||
.setUuid(ACI.from(uuid).toByteString())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun metadata(
|
|
||||||
address: AddressProto = address().build()
|
|
||||||
): MetadataProto.Builder {
|
|
||||||
return MetadataProto.newBuilder()
|
|
||||||
.setAddress(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun groupContextV2(
|
|
||||||
revision: Int = 0,
|
|
||||||
masterKeyBytes: ByteArray = Random.Default.nextBytes(GroupMasterKey.SIZE)
|
|
||||||
): GroupContextV2.Builder {
|
|
||||||
return GroupContextV2.newBuilder()
|
|
||||||
.setRevision(revision)
|
|
||||||
.setMasterKey(ByteString.copyFrom(masterKeyBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun storyContext(
|
|
||||||
sentTimestamp: Long = Random.nextLong(),
|
|
||||||
authorUuid: String = UUID.randomUUID().toString()
|
|
||||||
): DataMessage.StoryContext.Builder {
|
|
||||||
return DataMessage.StoryContext.newBuilder()
|
|
||||||
.setAuthorAci(authorUuid)
|
|
||||||
.setSentTimestamp(sentTimestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dataMessage(): DataMessage.Builder {
|
|
||||||
return DataMessage.newBuilder()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun content(): SignalServiceProtos.Content.Builder {
|
|
||||||
return SignalServiceProtos.Content.newBuilder()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun serviceContent(
|
|
||||||
localAddress: AddressProto = address().build(),
|
|
||||||
metadata: MetadataProto = metadata().build()
|
|
||||||
): SignalServiceContentProto.Builder {
|
|
||||||
return SignalServiceContentProto.newBuilder()
|
|
||||||
.setLocalAddress(localAddress)
|
|
||||||
.setMetadata(metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun <T> build(buildFn: TestProtos.() -> T): T {
|
|
||||||
return TestProtos().buildFn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms.testing
|
package org.thoughtcrime.securesms.testing
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.util.Base64
|
import org.hamcrest.Matcher
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.hamcrest.Matchers.hasSize
|
import org.hamcrest.Matchers.hasSize
|
||||||
import org.hamcrest.Matchers.`is`
|
import org.hamcrest.Matchers.`is`
|
||||||
import org.hamcrest.Matchers.not
|
import org.hamcrest.Matchers.not
|
||||||
import org.hamcrest.Matchers.notNullValue
|
import org.hamcrest.Matchers.notNullValue
|
||||||
import org.hamcrest.Matchers.nullValue
|
import org.hamcrest.Matchers.nullValue
|
||||||
|
import org.signal.core.util.Hex
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.readToList
|
import org.signal.core.util.readToList
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
@@ -56,33 +57,41 @@ infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
|
|||||||
assertThat(this, hasSize(expected))
|
assertThat(this, hasSize(expected))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
infix fun <T : Any> T.assert(matcher: Matcher<T>) {
|
||||||
|
assertThat(this, matcher)
|
||||||
|
}
|
||||||
|
|
||||||
fun CountDownLatch.awaitFor(duration: Duration) {
|
fun CountDownLatch.awaitFor(duration: Duration) {
|
||||||
if (!await(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS)) {
|
if (!await(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS)) {
|
||||||
throw TimeoutException("Latch await took longer than ${duration.inWholeMilliseconds}ms")
|
throw TimeoutException("Latch await took longer than ${duration.inWholeMilliseconds}ms")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dumpTableToLogs(tag: String = "TestUtils", table: String) {
|
fun dumpTableToLogs(tag: String = "TestUtils", table: String, columns: Set<String>? = null) {
|
||||||
dumpTable(table).forEach { Log.d(tag, it.toString()) }
|
dumpTable(table, columns).forEach { Log.d(tag, it.toString()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dumpTable(table: String): List<List<Pair<String, String?>>> {
|
fun dumpTable(table: String, columns: Set<String>?): List<List<Pair<String, String?>>> {
|
||||||
return SignalDatabase.rawDatabase
|
return SignalDatabase.rawDatabase
|
||||||
.select()
|
.select()
|
||||||
.from(table)
|
.from(table)
|
||||||
.run()
|
.run()
|
||||||
.readToList { cursor ->
|
.readToList { cursor ->
|
||||||
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
|
val map: List<Pair<String, String?>> = cursor.columnNames.mapNotNull { column ->
|
||||||
val index = cursor.getColumnIndex(column)
|
if (columns == null || columns.contains(column)) {
|
||||||
var data: String? = when (cursor.getType(index)) {
|
val index = cursor.getColumnIndex(column)
|
||||||
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
|
var data: String? = when (cursor.getType(index)) {
|
||||||
else -> cursor.getString(index)
|
Cursor.FIELD_TYPE_BLOB -> Hex.toStringCondensed(cursor.getBlob(index))
|
||||||
}
|
else -> cursor.getString(index)
|
||||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
}
|
||||||
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||||
}
|
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||||
|
}
|
||||||
|
|
||||||
column to data
|
column to data
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class that allows us to inject feature flags during tests.
|
|
||||||
*/
|
|
||||||
public final class FeatureFlagsAccessor {
|
|
||||||
|
|
||||||
public static void forceValue(String key, Object value) {
|
|
||||||
FeatureFlags.FORCED_VALUES.put(FeatureFlags.PHONE_NUMBER_PRIVACY, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,10 +38,8 @@ object MessageTableTestUtils {
|
|||||||
isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L}
|
isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L}
|
||||||
isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
|
isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
|
||||||
isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
|
isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
|
||||||
isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L}
|
|
||||||
isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
|
isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
|
||||||
isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L}
|
isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L}
|
||||||
isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L}
|
|
||||||
isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
|
isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
|
||||||
isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L}
|
isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L}
|
||||||
isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L}
|
isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package org.signal.benchmark
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.thoughtcrime.securesms.BuildConfig
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||||
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
@@ -16,15 +16,15 @@ import java.util.Optional
|
|||||||
class DummyAccountManagerFactory : AccountManagerFactory() {
|
class DummyAccountManagerFactory : AccountManagerFactory() {
|
||||||
override fun createAuthenticated(context: Context, aci: ACI, pni: PNI, number: String, deviceId: Int, password: String): SignalServiceAccountManager {
|
override fun createAuthenticated(context: Context, aci: ACI, pni: PNI, number: String, deviceId: Int, password: String): SignalServiceAccountManager {
|
||||||
return DummyAccountManager(
|
return DummyAccountManager(
|
||||||
ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number),
|
AppDependencies.signalServiceNetworkAccess.getConfiguration(number),
|
||||||
aci,
|
aci,
|
||||||
pni,
|
pni,
|
||||||
number,
|
number,
|
||||||
deviceId,
|
deviceId,
|
||||||
password,
|
password,
|
||||||
BuildConfig.SIGNAL_AGENT,
|
BuildConfig.SIGNAL_AGENT,
|
||||||
FeatureFlags.okHttpAutomaticRetry(),
|
RemoteConfig.okHttpAutomaticRetry,
|
||||||
FeatureFlags.groupLimits().hardLimit
|
RemoteConfig.groupLimits.hardLimit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package org.signal.benchmark.setup
|
package org.signal.benchmark.setup
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.attachments.Cdn
|
||||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
import org.thoughtcrime.securesms.database.MessageType
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.TestDbUtils
|
import org.thoughtcrime.securesms.database.TestDbUtils
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||||
@@ -65,7 +66,8 @@ object TestMessages {
|
|||||||
return insert
|
return insert
|
||||||
}
|
}
|
||||||
fun insertIncomingTextMessage(other: Recipient, body: String, timestamp: Long? = null) {
|
fun insertIncomingTextMessage(other: Recipient, body: String, timestamp: Long? = null) {
|
||||||
val message = IncomingMediaMessage(
|
val message = IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = other.id,
|
from = other.id,
|
||||||
body = body,
|
body = body,
|
||||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||||
@@ -73,10 +75,11 @@ object TestMessages {
|
|||||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis()
|
receivedTimeMillis = timestamp ?: System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
|
|
||||||
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get().messageId
|
SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get().messageId
|
||||||
}
|
}
|
||||||
fun insertIncomingQuoteTextMessage(other: Recipient, body: String, quote: QuoteModel, timestamp: Long?) {
|
fun insertIncomingQuoteTextMessage(other: Recipient, body: String, quote: QuoteModel, timestamp: Long?) {
|
||||||
val message = IncomingMediaMessage(
|
val message = IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = other.id,
|
from = other.id,
|
||||||
body = body,
|
body = body,
|
||||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||||
@@ -90,28 +93,30 @@ object TestMessages {
|
|||||||
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
|
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
|
||||||
imageAttachment()
|
imageAttachment()
|
||||||
}
|
}
|
||||||
val message = IncomingMediaMessage(
|
val message = IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = other.id,
|
from = other.id,
|
||||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||||
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||||
)
|
)
|
||||||
return insertIncomingMediaMessage(recipient = other, message = message, failed = failed)
|
return insertIncomingMessage(recipient = other, message = message, failed = failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertIncomingVoiceMessage(other: Recipient, timestamp: Long? = null): Long {
|
fun insertIncomingVoiceMessage(other: Recipient, timestamp: Long? = null): Long {
|
||||||
val message = IncomingMediaMessage(
|
val message = IncomingMessage(
|
||||||
|
type = MessageType.NORMAL,
|
||||||
from = other.id,
|
from = other.id,
|
||||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||||
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||||
attachments = PointerAttachment.forPointers(Optional.of(Collections.singletonList(voiceAttachment()) as List<SignalServiceAttachment>))
|
attachments = PointerAttachment.forPointers(Optional.of(Collections.singletonList(voiceAttachment()) as List<SignalServiceAttachment>))
|
||||||
)
|
)
|
||||||
return insertIncomingMediaMessage(recipient = other, message = message, failed = false)
|
return insertIncomingMessage(recipient = other, message = message, failed = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertIncomingMediaMessage(recipient: Recipient, message: IncomingMediaMessage, failed: Boolean = false): Long {
|
private fun insertIncomingMessage(recipient: Recipient, message: IncomingMessage, failed: Boolean = false): Long {
|
||||||
val id = insertIncomingMessage(recipient = recipient, message = message)
|
val id = insertIncomingMessage(recipient = recipient, message = message)
|
||||||
if (failed) {
|
if (failed) {
|
||||||
setMessageMediaFailed(id)
|
setMessageMediaFailed(id)
|
||||||
@@ -122,8 +127,8 @@ object TestMessages {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertIncomingMessage(recipient: Recipient, message: IncomingMediaMessage): Long {
|
private fun insertIncomingMessage(recipient: Recipient, message: IncomingMessage): Long {
|
||||||
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(recipient)).get().messageId
|
return SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(recipient)).get().messageId
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setMessageMediaFailed(messageId: Long) {
|
private fun setMessageMediaFailed(messageId: Long) {
|
||||||
@@ -139,7 +144,7 @@ object TestMessages {
|
|||||||
}
|
}
|
||||||
private fun imageAttachment(): SignalServiceAttachmentPointer {
|
private fun imageAttachment(): SignalServiceAttachmentPointer {
|
||||||
return SignalServiceAttachmentPointer(
|
return SignalServiceAttachmentPointer(
|
||||||
ReleaseChannel.CDN_NUMBER,
|
Cdn.S3.cdnNumber,
|
||||||
SignalServiceAttachmentRemoteId.from(""),
|
SignalServiceAttachmentRemoteId.from(""),
|
||||||
"image/webp",
|
"image/webp",
|
||||||
null,
|
null,
|
||||||
@@ -149,19 +154,21 @@ object TestMessages {
|
|||||||
1024,
|
1024,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
|
0,
|
||||||
Optional.of("/not-there.jpg"),
|
Optional.of("/not-there.jpg"),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis(),
|
||||||
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun voiceAttachment(): SignalServiceAttachmentPointer {
|
private fun voiceAttachment(): SignalServiceAttachmentPointer {
|
||||||
return SignalServiceAttachmentPointer(
|
return SignalServiceAttachmentPointer(
|
||||||
ReleaseChannel.CDN_NUMBER,
|
Cdn.S3.cdnNumber,
|
||||||
SignalServiceAttachmentRemoteId.from(""),
|
SignalServiceAttachmentRemoteId.from(""),
|
||||||
"audio/aac",
|
"audio/aac",
|
||||||
null,
|
null,
|
||||||
@@ -171,13 +178,15 @@ object TestMessages {
|
|||||||
1024,
|
1024,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
|
0,
|
||||||
Optional.of("/not-there.aac"),
|
Optional.of("/not-there.aac"),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis(),
|
||||||
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
|||||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
|
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
@@ -35,7 +35,7 @@ object TestUsers {
|
|||||||
private var generatedOthers: Int = 0
|
private var generatedOthers: Int = 0
|
||||||
|
|
||||||
fun setupSelf(): Recipient {
|
fun setupSelf(): Recipient {
|
||||||
val application: Application = ApplicationDependencies.getApplication()
|
val application: Application = AppDependencies.application
|
||||||
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
|
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
|
||||||
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
|
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
|
||||||
@@ -44,8 +44,8 @@ object TestUsers {
|
|||||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||||
|
|
||||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||||
|
|
||||||
val registrationRepository = RegistrationRepository(application)
|
val registrationRepository = RegistrationRepository(application)
|
||||||
val registrationData = RegistrationData(
|
val registrationData = RegistrationData(
|
||||||
@@ -63,8 +63,8 @@ object TestUsers {
|
|||||||
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
||||||
masterKey = null,
|
masterKey = null,
|
||||||
pin = null,
|
pin = null,
|
||||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
|
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
|
||||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
|
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
|
||||||
)
|
)
|
||||||
|
|
||||||
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
|
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
|
||||||
@@ -77,7 +77,7 @@ object TestUsers {
|
|||||||
|
|
||||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||||
|
|
||||||
SignalStore.svr().optOut()
|
SignalStore.svr.optOut()
|
||||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||||
|
|
||||||
@@ -100,11 +100,11 @@ object TestUsers {
|
|||||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
|
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
|
||||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
||||||
|
|
||||||
others += recipientId
|
others += recipientId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
|
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
|
||||||
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
|
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
|
||||||
import org.thoughtcrime.securesms.database.MessageTypes
|
import org.thoughtcrime.securesms.database.MessageTypes
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.StoryType
|
import org.thoughtcrime.securesms.database.model.StoryType
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
@@ -78,7 +78,7 @@ class ConversationElementGenerator {
|
|||||||
|
|
||||||
val isIncoming = random.nextBoolean()
|
val isIncoming = random.nextBoolean()
|
||||||
|
|
||||||
val record = MediaMmsMessageRecord(
|
val record = MmsMessageRecord(
|
||||||
messageId,
|
messageId,
|
||||||
if (isIncoming) Recipient.UNKNOWN else Recipient.self(),
|
if (isIncoming) Recipient.UNKNOWN else Recipient.self(),
|
||||||
0,
|
0,
|
||||||
@@ -86,7 +86,7 @@ class ConversationElementGenerator {
|
|||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
1,
|
true,
|
||||||
1,
|
1,
|
||||||
testMessage,
|
testMessage,
|
||||||
SlideDeck(),
|
SlideDeck(),
|
||||||
@@ -97,7 +97,7 @@ class ConversationElementGenerator {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
1,
|
true,
|
||||||
null,
|
null,
|
||||||
emptyList(),
|
emptyList(),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
@@ -106,7 +106,7 @@ class ConversationElementGenerator {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
now,
|
now,
|
||||||
1,
|
true,
|
||||||
now,
|
now,
|
||||||
null,
|
null,
|
||||||
StoryType.NONE,
|
StoryType.NONE,
|
||||||
@@ -117,11 +117,13 @@ class ConversationElementGenerator {
|
|||||||
-1,
|
-1,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
0
|
0,
|
||||||
|
false,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
|
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
|
||||||
ApplicationDependencies.getApplication(),
|
AppDependencies.application,
|
||||||
record,
|
record,
|
||||||
Recipient.UNKNOWN
|
Recipient.UNKNOWN
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.navGraphViewModels
|
import androidx.navigation.navGraphViewModels
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||||
@@ -33,6 +34,7 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
|||||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
|
||||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
@@ -41,7 +43,6 @@ import org.thoughtcrime.securesms.groups.GroupId
|
|||||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||||
@@ -61,11 +62,13 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val adapter = ConversationAdapterV2(
|
val adapter = ConversationAdapterV2(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
glideRequests = GlideApp.with(this),
|
requestManager = Glide.with(this),
|
||||||
clickListener = ClickListener(),
|
clickListener = ClickListener(),
|
||||||
hasWallpaper = springboardViewModel.hasWallpaper.value,
|
hasWallpaper = springboardViewModel.hasWallpaper.value,
|
||||||
colorizer = Colorizer(),
|
colorizer = Colorizer(),
|
||||||
startExpirationTimeout = {}
|
startExpirationTimeout = {},
|
||||||
|
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) },
|
||||||
|
displayDialogFragment = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (springboardViewModel.hasWallpaper.value) {
|
if (springboardViewModel.hasWallpaper.value) {
|
||||||
@@ -228,6 +231,10 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
|||||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onChangeProfileNameUpdateContact(recipient: Recipient) {
|
||||||
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCallToAction(action: String) {
|
override fun onCallToAction(action: String) {
|
||||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -277,7 +284,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
|||||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
|
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) {
|
||||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,5 +303,25 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
|||||||
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
|
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
|
||||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onItemDoubleClick(item: MultiselectPart) {
|
||||||
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPaymentTombstoneClicked() {
|
||||||
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowSafetyTips(forGroup: Boolean) {
|
||||||
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReportSpamLearnMoreClicked() {
|
||||||
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageRequestAcceptOptionsClicked() {
|
||||||
|
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:replace="android:usesCleartextTraffic"
|
tools:replace="android:usesCleartextTraffic"
|
||||||
|
|||||||
@@ -22,17 +22,10 @@
|
|||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||||
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
|
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
|
||||||
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
||||||
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
|
|
||||||
tools:ignore="ProtectedPermissions"/>
|
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
|
|
||||||
<uses-permission android:name="android.permission.READ_SMS"/>
|
|
||||||
<uses-permission android:name="android.permission.SEND_SMS"/>
|
|
||||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||||
|
|
||||||
@@ -50,16 +43,10 @@
|
|||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
|
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
|
||||||
|
|
||||||
<!-- For sending/receiving events -->
|
|
||||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
|
||||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Normal -->
|
<!-- Normal -->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
@@ -76,17 +63,14 @@
|
|||||||
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
|
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
|
||||||
<!-- For fixing MMS -->
|
<!-- For device transfer -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<!-- Set image as wallpaper -->
|
|
||||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
|
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
|
||||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||||
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||||
@@ -160,12 +144,6 @@
|
|||||||
android:value=".MainActivity" />
|
android:value=".MainActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".PromptMmsActivity"
|
|
||||||
android:label="Configure MMS Settings"
|
|
||||||
android:windowSoftInputMode="stateUnchanged"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<activity android:name=".DeviceProvisioningActivity"
|
<activity android:name=".DeviceProvisioningActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
@@ -185,10 +163,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".preferences.MmsPreferencesActivity"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
|
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
@@ -252,6 +226,7 @@
|
|||||||
|
|
||||||
<activity-alias android:name=".RoutingActivity"
|
<activity-alias android:name=".RoutingActivity"
|
||||||
android:targetActivity=".MainActivity"
|
android:targetActivity=".MainActivity"
|
||||||
|
android:resizeableActivity="true"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -593,6 +568,13 @@
|
|||||||
android:host="signal.group"/>
|
android:host="signal.group"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="https" android:host="signaldonations.org" android:pathPrefix="/stripe/return/ideal"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -630,6 +612,7 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:parentActivityName=".MainActivity"
|
android:parentActivityName=".MainActivity"
|
||||||
|
android:resizeableActivity="true"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
@@ -678,13 +661,18 @@
|
|||||||
|
|
||||||
<activity android:name=".PassphrasePromptActivity"
|
<activity android:name=".PassphrasePromptActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/TextSecure.LightIntroTheme"
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".NewConversationActivity"
|
<activity android:name=".NewConversationActivity"
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
android:windowSoftInputMode="stateAlwaysVisible"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<activity android:name=".recipients.ui.findby.FindByActivity"
|
||||||
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
@@ -716,6 +704,7 @@
|
|||||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
@@ -801,13 +790,6 @@
|
|||||||
android:windowSoftInputMode="stateAlwaysHidden"
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".badges.gifts.flow.GiftFlowActivity"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
|
||||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
|
||||||
android:windowSoftInputMode="stateAlwaysHidden"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".wallpaper.ChatWallpaperActivity"
|
android:name=".wallpaper.ChatWallpaperActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
@@ -834,7 +816,14 @@
|
|||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".registration.RegistrationNavigationActivity"
|
<activity android:name=".registration.ui.RegistrationActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
|
android:windowSoftInputMode="stateHidden"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<activity android:name=".restore.RestoreActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
@@ -930,19 +919,25 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
|
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:permission="android.permission.CALL_PHONE"
|
android:permission="android.permission.CALL_PHONE"
|
||||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter tools:ignore="AppLinkUrlError">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" />
|
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter tools:ignore="AppLinkUrlError">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.videocall" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||||
@@ -961,17 +956,21 @@
|
|||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".profiles.edit.EditProfileActivity"
|
<activity android:name=".profiles.edit.CreateProfileActivity"
|
||||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".profiles.username.AddAUsernameActivity"
|
<activity android:name=".registration.ui.restore.RemoteRestoreActivity"
|
||||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<activity android:name=".profiles.manage.EditProfileActivity"
|
||||||
|
android:theme="@style/TextSecure.LightTheme"
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".profiles.manage.ManageProfileActivity"
|
<activity android:name=".nicknames.NicknameActivity"
|
||||||
android:theme="@style/TextSecure.LightTheme"
|
android:theme="@style/TextSecure.LightTheme"
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
@@ -997,7 +996,7 @@
|
|||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
|
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
|
||||||
android:theme="@style/Theme.AppCompat.Dialog.Alert"
|
android:theme="@style/TextSecure.DialogActivity"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".contactshare.ContactShareEditActivity"
|
<activity android:name=".contactshare.ContactShareEditActivity"
|
||||||
@@ -1029,6 +1028,7 @@
|
|||||||
<activity android:name=".MainActivity"
|
<activity android:name=".MainActivity"
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:resizeableActivity="true"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".pin.PinRestoreActivity"
|
<activity android:name=".pin.PinRestoreActivity"
|
||||||
@@ -1062,13 +1062,6 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".megaphone.SmsExportMegaphoneActivity"
|
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<activity android:name=".ratelimit.RecaptchaProofActivity"
|
<activity android:name=".ratelimit.RecaptchaProofActivity"
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||||
@@ -1085,28 +1078,25 @@
|
|||||||
android:theme="@style/Theme.Signal.WallpaperCropper"
|
android:theme="@style/Theme.Signal.WallpaperCropper"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<activity android:name=".components.settings.app.usernamelinks.main.QrImageSelectionActivity"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrScannerActivity"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".reactions.edit.EditReactionsActivity"
|
<activity android:name=".reactions.edit.EditReactionsActivity"
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity android:name=".exporter.flow.SmsExportActivity"
|
<activity android:name=".components.settings.app.subscription.donate.CheckoutFlowActivity"
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:screenOrientation="portrait"
|
android:exported="false"/>
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
|
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:enabled="true"
|
|
||||||
android:name=".exporter.SignalSmsExportService"
|
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
@@ -1154,19 +1144,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service android:name=".service.QuickResponseService"
|
|
||||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
|
||||||
android:exported="true" >
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="sms" />
|
|
||||||
<data android:scheme="smsto" />
|
|
||||||
<data android:scheme="mms" />
|
|
||||||
<data android:scheme="mmsto" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service android:name=".service.AccountAuthenticatorService" android:exported="true">
|
<service android:name=".service.AccountAuthenticatorService" android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.accounts.AccountAuthenticator" />
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
@@ -1186,6 +1163,14 @@
|
|||||||
android:name=".service.GenericForegroundService"
|
android:name=".service.GenericForegroundService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.AttachmentProgressService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.BackupProgressService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".gcm.FcmFetchBackgroundService"
|
android:name=".gcm.FcmFetchBackgroundService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
@@ -1200,39 +1185,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:name=".service.SmsListener"
|
|
||||||
android:permission="android.permission.BROADCAST_SMS"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter android:priority="1001">
|
|
||||||
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.provider.Telephony.SMS_DELIVER"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver android:name=".service.SmsDeliveryListener"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.thoughtcrime.securesms.services.MESSAGE_SENT"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver android:name=".service.MmsListener"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.BROADCAST_WAP_PUSH">
|
|
||||||
<intent-filter android:priority="1001">
|
|
||||||
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED"/>
|
|
||||||
<data android:mimeType="application/vnd.wap.mms-message" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
|
|
||||||
<data android:mimeType="application/vnd.wap.mms-message" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver android:name=".notifications.MarkReadReceiver"
|
<receiver android:name=".notifications.MarkReadReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
@@ -1292,11 +1244,6 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true" />
|
android:grantUriPermissions="true" />
|
||||||
|
|
||||||
<provider android:name=".providers.MmsBodyProvider"
|
|
||||||
android:grantUriPermissions="true"
|
|
||||||
android:exported="false"
|
|
||||||
android:authorities="${applicationId}.mms" />
|
|
||||||
|
|
||||||
<provider android:name="androidx.core.content.FileProvider"
|
<provider android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -1344,6 +1291,12 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
|
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
|
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
|
||||||
@@ -1408,6 +1361,20 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="camera|microphone" />
|
||||||
|
|
||||||
|
<receiver android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallServiceReceiver" android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.HANGUP"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
|
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 111 KiB |