Compare commits
463 Commits
Author | SHA1 | Date |
---|---|---|
Andrew | bdb6e7d12b | |
Andrew | 377460a60f | |
andrew | a57b7ef121 | |
Andrew | b96a5c561e | |
0x330a | 9a83daa53f | |
wafflesvsfrankie | 7fe40ea9f1 | |
0x330a | 9d02eb33c7 | |
0x330a | b6bb586509 | |
0x330a | 82cbf830ae | |
0x330a | c1102a2a50 | |
0x330a | 862a47e7e3 | |
0x330a | 6f22eb659b | |
0x330a | cb1b5b0f78 | |
0x330a | 20df73355d | |
0x330a | 84bc1dcb6b | |
0x330a | 77a18e337b | |
0x330a | 443ddfa370 | |
Andrew | e124d442ef | |
Andrew | 698b853716 | |
andrew | 0cd0ac9c75 | |
Kee Jefferys | 2093cbc5e4 | |
Andrew | 984c3763b6 | |
Andrew | 833b30fc14 | |
0x330a | 380d6694ea | |
Andrew | b4eb54ee89 | |
andrew | 303aacb2e3 | |
hjubb | 99e5ed3db7 | |
0x330a | 29275cef51 | |
0x330a | 4daa3e6923 | |
Andrew | 99dca1cda7 | |
andrew | 0e0cbf112b | |
0x330a | 2466d9b4c0 | |
Andrew | f6345c86ce | |
andrew | 7861eb25c2 | |
andrew | 296c5d743f | |
andrew | ae9d3810e1 | |
andrew | 550955f530 | |
andrew | 62cd0f68f0 | |
andrew | d308f381d9 | |
andrew | 0aa5dc7969 | |
andrew | 7c8882e1f3 | |
andrew | b09b6836d4 | |
andrew | 4c8f38df72 | |
andrew | b987ba719b | |
andrew | ed7ce36402 | |
andrew | 1d9fb13809 | |
andrew | 9899b37f43 | |
andrew | 77100231d2 | |
andrew | 16177d5cb1 | |
andrew | 9813b526f0 | |
Andrew | c9417b2fec | |
andrew | 309293df63 | |
Andrew | 34fc6ee6cb | |
andrew | e1e5c5937b | |
andrew | e60c05cee0 | |
andrew | 5a5b2f593f | |
Andrew | 2f42fe9d0d | |
andrew | c8dcfbf32c | |
Andrew | 9cf99480d6 | |
andrew | d6380c5e63 | |
andrew | 24f7bb2b45 | |
andrew | bcf925c132 | |
andrew | a27f81db30 | |
andrew | cc6f880665 | |
andrew | 41d24ef2c3 | |
andrew | 7ee9b14247 | |
Andrew | cd1a52399e | |
andrew | d1e22ca369 | |
andrew | 7be1f092f9 | |
andrew | 4738c9b4f9 | |
andrew | d3ea4e2e30 | |
Andrew | 55216875ac | |
andrew | 34990b13d3 | |
andrew | 01e9d15872 | |
andrew | 58cda9ba4a | |
andrew | 002793baed | |
Andrew | d39cf2754c | |
andrew | fbb2172739 | |
andrew | d0415c5bf1 | |
Andrew | 9af6dc9265 | |
Andrew | fbf448f889 | |
andrew | b25f0e6d27 | |
andrew | 47d1c657f3 | |
andrew | fc8a92998c | |
andrew | 452db6dfa3 | |
andrew | 9a84f6c67b | |
hjubb | 1bb3939930 | |
hjubb | e7608763a0 | |
hjubb | d4ab49ebbb | |
hjubb | 9523953bd9 | |
hjubb | 976500e8f9 | |
0x330a | ac18f1cbfe | |
Andrew | 96ec733517 | |
andrew | adfa94614b | |
andrew | c3ef4d6e7b | |
andrew | d83532b6af | |
andrew | 1845b60dac | |
andrew | 172f85ae4f | |
andrew | fb68aaede6 | |
andrew | 09b321530d | |
andrew | a1e8ad2c37 | |
andrew | 821327569e | |
andrew | b26c98af68 | |
andrew | 0824713ac5 | |
andrew | bbc9cdfeeb | |
andrew | d8b85768d2 | |
andrew | e5b19d4ea4 | |
andrew | fc87ae18f5 | |
andrew | 1d1977ca7a | |
andrew | c417b37236 | |
andrew | 8be1e8e87e | |
andrew | 68684bb839 | |
andrew | efb5b27191 | |
andrew | 8d66d948ca | |
andrew | d6b1440217 | |
andrew | 7cd2bd0e0d | |
andrew | 6209ae68a8 | |
andrew | ee0141f82d | |
andrew | f82ed7718d | |
Andrew | 7da3e4f022 | |
andrew | 26aed783e8 | |
andrew | 6890f5c448 | |
andrew | d719660030 | |
andrew | 0fcd997290 | |
andrew | 70e63a23bc | |
andrew | 0ec93e4b36 | |
andrew | 1d29b5465f | |
andrew | db4ff94084 | |
andrew | 1902d4755c | |
Stefan Junker | ba4143c298 | |
andrew | 1303979cdf | |
andrew | 4decce9dde | |
andrew | d44dbe089f | |
andrew | 351b259449 | |
andrew | 19b2c5ef97 | |
andrew | f68c01b2ee | |
andrew | 92fae3d6bf | |
andrew | 876e12c411 | |
andrew | 6d596226b3 | |
Andrew | c039eb89bc | |
andrew | 676c29ca60 | |
andrew | ac476f4382 | |
andrew | d5b3d9bcf9 | |
andrew | 0c2682fe47 | |
andrew | fc108b34db | |
andrew | ab8b2c42b9 | |
andrew | 97297508f4 | |
andrew | 20eac67761 | |
andrew | 4ee8dc712e | |
andrew | b46b52ace4 | |
andrew | 8be088ad56 | |
andrew | dc7602a1d3 | |
andrew | e3f60eb5f2 | |
andrew | 42cfce0c3e | |
andrew | 0e0ab9151e | |
andrew | 5c9dc36460 | |
Andrew | b9f24bc4bd | |
Andrew | 92a6447b8a | |
andrew | 667af27bfb | |
andrew | 153aa4ceaa | |
andrew | 440a5a942d | |
Andrew | cc015c45bd | |
andrew | 288b70bb14 | |
andrew | b2a1b5fe46 | |
andrew | be4d742e84 | |
andrew | 01d80ae54b | |
andrew | 3f6229f841 | |
andrew | ba6eca2443 | |
Morgan Pretty | 300c3a6605 | |
Morgan Pretty | f0486061b1 | |
Morgan Pretty | 026b994664 | |
Morgan Pretty | a957e78aac | |
Morgan Pretty | 80104f6db8 | |
Morgan Pretty | 7699e47f7b | |
Morgan Pretty | 6f28b41b53 | |
Morgan Pretty | 11c1fd382d | |
Kee Jefferys | fef1fbd57c | |
andrew | 1efda68334 | |
Morgan Pretty | 22a30f1907 | |
Morgan Pretty | 082c087105 | |
Morgan Pretty | 9ce89087a5 | |
Morgan Pretty | cea65f3e45 | |
Morgan Pretty | 429b496a22 | |
Morgan Pretty | 6edf0d46f8 | |
Morgan Pretty | 31608111b3 | |
Morgan Pretty | 7f7ab63d15 | |
Morgan Pretty | f1686ea260 | |
andrew | e5db7fc886 | |
Morgan Pretty | 9d5fa1239c | |
Morgan Pretty | 346bdc1774 | |
Morgan Pretty | 6c14ed26e2 | |
Morgan Pretty | f21100eff7 | |
andrew | a6cfe5817d | |
Morgan Pretty | 6b006b9d91 | |
Morgan Pretty | c92ef09d09 | |
andrew | e99133fd8b | |
andrew | 09b241ba67 | |
Morgan Pretty | 1980113e41 | |
andrew | 94b48d5fb9 | |
andrew | 975f9fc4d1 | |
andrew | 033eabbc53 | |
andrew | 4641512644 | |
Morgan Pretty | 2b7bd7417e | |
andrew | cbe90e7bfc | |
andrew | 6d528d0e92 | |
andrew | 72c07f4b99 | |
andrew | 80f21aeb41 | |
andrew | c5f821add0 | |
andrew | 50271685af | |
andrew | 623cfde8b1 | |
andrew | 391735dc28 | |
andrew | 4e92c40210 | |
andrew | 1d8d678047 | |
andrew | 4ee68cbbb1 | |
andrew | 1b88d4f950 | |
Morgan Pretty | 9b7fb3dd86 | |
Morgan Pretty | 8ce6e997aa | |
Morgan Pretty | b7744f4f2d | |
Morgan Pretty | c77d465438 | |
Morgan Pretty | 0c2a635d03 | |
Morgan Pretty | 10af2815ac | |
Andrew | e8d26222b9 | |
andrew | 0af713317a | |
Morgan Pretty | 22ed2dd8aa | |
Morgan Pretty | d541f395a5 | |
andrew | 03aa19aae4 | |
Andrew | 5519f17775 | |
Andrew | 7d31af9eb0 | |
Andrew | 331d523c45 | |
Andrew | 47d5b242ca | |
Andrew | 1b231bfff3 | |
0x330a | 95bb9ee441 | |
Andrew | 945df19aef | |
Andrew | 95a165aa05 | |
andrew | 55dd62240a | |
andrew | ff124b8edc | |
andrew | a295dfb248 | |
andrew | 8b39c4e56a | |
andrew | 76466e57de | |
andrew | 4f2ef7f2af | |
andrew | 8ef6ec2125 | |
Andrew | d505109aa3 | |
Andrew | a8e77659b3 | |
Andrew | 8f55ac93f8 | |
Andrew | 6fc7b3591e | |
andrew | 30d748e147 | |
Jason Rhinelander | 46acd7878d | |
andrew | 235b94a905 | |
Andrew | f64fe4b652 | |
Andrew | c31d4d6c31 | |
andrew | 8c5ff1f944 | |
andrew | a334b8912a | |
Andrew | ab3caf8ab5 | |
Andrew | 44cb8fec2c | |
Andrew | e6dc5f2128 | |
Andrew | fef2948f58 | |
Andrew | 57476cd56e | |
Andrew | ba9dab33c5 | |
Andrew | aadba75038 | |
Andrew | 84004d2fdb | |
Andrew | 910ab8874e | |
ryanzhao | a7e0bd5366 | |
ryanzhao | 88e788a406 | |
Andrew | 8dbabec4e7 | |
Andrew | 2b00729df3 | |
Andrew | ba3566f7e8 | |
ryanzhao | 89545f0406 | |
andrew | d20c27d6d3 | |
andrew | 4469d9754a | |
andrew | c8fa2d8d6e | |
ryanzhao | fa71ea1850 | |
andrew | 8d38d1c0fb | |
Ryan Zhao | b494088c3d | |
andrew | b6667b83ce | |
andrew | f0715f16e0 | |
Ryan Zhao | 2ceb9e2bf4 | |
Ryan Zhao | 2b48b52df0 | |
andrew | ec2abffdcc | |
andrew | e9a15941ae | |
ryanzhao | 375815c719 | |
andrew | 6a5d97a0f0 | |
andrew | 9e6d1e27fc | |
andrew | a9078c8d08 | |
andrew | a152250a60 | |
andrew | 24741fcc22 | |
andrew | d3ce899a80 | |
andrew | 45eb3549f6 | |
ryanzhao | d868021f0a | |
ryanzhao | 7a14c3f8be | |
Andrew | 99cb10f5be | |
Andrew | 83b6002a27 | |
Andrew | a934c5c2e2 | |
Andrew | 70aab2994b | |
Ryan Zhao | 2630c97a4e | |
ryanzhao | bd4f451513 | |
andrew | 58e532f0ec | |
Andrew Gallasch | ffef98ecc9 | |
Andrew Gallasch | 5d552a7f93 | |
Andrew Gallasch | 716fc1f4fa | |
Andrew Gallasch | 7d33177e06 | |
Ryan Zhao | c7bbdb778b | |
Ryan Zhao | 51856138e3 | |
0x330a | d2e80c3157 | |
0x330a | 7762d534bb | |
0x330a | 8d4f2445f2 | |
0x330a | 2246a5d9ce | |
Andrew Gallasch | 63d442584c | |
SzBenedek2006 | 7b0c014791 | |
hjubb | e1ff2bf988 | |
0x330a | b25eb9af8e | |
wafflesvsfrankie | 9ad73139b6 | |
hjubb | cfdc3dc24d | |
hjubb | 2a2f90276d | |
Morgan Pretty | eb739bdc9b | |
Morgan Pretty | 5e28af2be4 | |
Ian Macdonald | 405b8c7d28 | |
Float-hu | a062e1c2b2 | |
Morgan Pretty | 391418ae1e | |
Morgan Pretty | c8846bc31a | |
Morgan Pretty | 5bd188387b | |
Morgan Pretty | 7df7412a77 | |
Morgan Pretty | 5949a158d5 | |
Morgan Pretty | cc7a80900f | |
Morgan Pretty | da0d38f389 | |
Morgan Pretty | 4df530d341 | |
Qian Hong | 7e7c016bc4 | |
Qian Hong | 47066a320c | |
Morgan Pretty | bbedad5ebb | |
Morgan Pretty | 9fd68d27f8 | |
Morgan Pretty | 97458a4baa | |
Morgan Pretty | cd3b8f3571 | |
hjubb | 48799db21c | |
hjubb | 378fb40984 | |
0x330a | 395ada62ff | |
Morgan Pretty | 40f315af8e | |
Morgan Pretty | 50989cb2ee | |
Morgan Pretty | 0256735135 | |
hjubb | 4e38b75f57 | |
Morgan Pretty | cf916a5762 | |
Morgan Pretty | 7ffe48b5ed | |
Morgan Pretty | 0a391b8c88 | |
Morgan Pretty | 5d7d141d0c | |
Morgan Pretty | 081d1c4e93 | |
Morgan Pretty | 30615be029 | |
Morgan Pretty | abf733fbdb | |
Morgan Pretty | 87f1d708b1 | |
Morgan Pretty | f8c700793a | |
Morgan Pretty | 05838faaf0 | |
Morgan Pretty | fff0e5a32d | |
Morgan Pretty | 2711a6dd5f | |
Morgan Pretty | 1ce9e47c51 | |
Morgan Pretty | 63debb34e6 | |
Morgan Pretty | ce3aa980aa | |
Morgan Pretty | 251df065f8 | |
Morgan Pretty | e44ae140e0 | |
Morgan Pretty | beabc1c686 | |
Morgan Pretty | bc20811431 | |
Morgan Pretty | dc9458f313 | |
Morgan Pretty | 23dca5b38d | |
Morgan Pretty | 025989f928 | |
Morgan Pretty | bdac7f2ea3 | |
hjubb | f0e67fd86a | |
Morgan Pretty | e0785c4854 | |
0x330a | 5861623369 | |
Morgan Pretty | ebe8479e4c | |
0x330a | 86b065203f | |
Morgan Pretty | a6f09c6fef | |
Morgan Pretty | 8a4a9623cc | |
Morgan Pretty | 0ed5c5825d | |
Morgan Pretty | afa42daab1 | |
Morgan Pretty | 810430e806 | |
Morgan Pretty | ce8e5c596e | |
Morgan Pretty | 694ca79958 | |
Morgan Pretty | cae15a200d | |
Morgan Pretty | a2fcb3195d | |
Morgan Pretty | f4fdfd7410 | |
Morgan Pretty | cc5c63b211 | |
Morgan Pretty | 70f0dad36e | |
Morgan Pretty | 693c3a9656 | |
Morgan Pretty | f9ff3feb29 | |
Morgan Pretty | afdf730eaa | |
Morgan Pretty | 3e68bdc2f8 | |
Morgan Pretty | 5afd647686 | |
Morgan Pretty | d0a4bac83e | |
Morgan Pretty | a1b052ef82 | |
Morgan Pretty | e7b6ddacbb | |
Morgan Pretty | c0bef51fe0 | |
Morgan Pretty | d68d26cd5d | |
Morgan Pretty | 5abc3119cb | |
Morgan Pretty | e6fe38587b | |
Morgan Pretty | 12205e72b6 | |
Morgan Pretty | 1a28fd2a9e | |
hjubb | df8a6d739a | |
0x330a | cdd2559839 | |
hjubb | bda50d263c | |
Ninad Bhuiyan | 95298bb9e3 | |
0x330a | d9a815a729 | |
0x330a | 6fbfc95ad2 | |
0x330a | 7fcd754f00 | |
charles | 752c25d627 | |
charles | 76fff8bc74 | |
ceokot | 427b2d7b5f | |
ceokot | 1dbcffe40b | |
charles | 10d0134269 | |
charles | db61d693c4 | |
charles | 2216c99dcd | |
ceokot | 3018937ad9 | |
ceokot | 38cc42a162 | |
Morgan Pretty | aecb40cce9 | |
Harris | 7a773016da | |
jubb | 92075aed32 | |
charles | eb74f901c1 | |
charles | 140877f4e3 | |
charles | 1f7edadc59 | |
charles | c537da6acd | |
charles | 99f70e5c21 | |
charles | ffa280bc1b | |
Harris | 54efc36a8b | |
ceokot | fbd1721eaf | |
jubb | 42b2271336 | |
Harris | d2dc86de88 | |
jubb | 3fcd972c2a | |
Harris | b6106d5506 | |
jubb | c09e4e4907 | |
jubb | 87c1ede75c | |
jubb | 2c3a949bb3 | |
Harris | bd51cbf6fd | |
Harris | 9f8ed4daf2 | |
Harris | aa43ab2a2e | |
Harris | 919bb01d58 | |
Harris | bdc4f5aebe | |
Harris | 29124f36b6 | |
Harris | 7d186c198e | |
Harris | 361ff8370b | |
Harris | b1918f07e1 | |
jubb | 16d4519d7e | |
Harris | ebcae1f284 | |
Harris | 2520287aff | |
Harris | 2bd078a441 | |
ceokot | 16ca97d2d3 | |
Harris | d6d0c52745 | |
ceokot | 2bfc8215d4 | |
Harris | 5469f232a0 | |
Harris | e7ca53ff72 | |
Harris | c65feba683 | |
Harris | 7dcb566a57 | |
jubb | 25eff4fece | |
jubb | 865a69c49f | |
jubb | 6c07121d7a | |
jubb | 997389b940 | |
jubb | 2f80fac57e | |
jubb | 3944d5d1df | |
jubb | 44fc0fc2dd | |
jubb | 00f28b7360 | |
jubb | 1ba204855c | |
jubb | 4007875f07 | |
ceokot | bee287bb7e | |
Harris | d53752713e | |
jubb | b1e954084c | |
Harris | 2ea7f638d8 | |
Harris | d3e2ef0b40 | |
Harris | 344e7f333e | |
jubb | ba60e8a8ee |
|
@ -1,45 +0,0 @@
|
||||||
<!-- This is a bug report template. By following the instructions below and filling out the sections with your information, you will help the developers get all the necessary data to fix your issue.
|
|
||||||
You can also preview your report before submitting it. You may remove sections that aren't relevant to your particular case.
|
|
||||||
|
|
||||||
Before we begin, please note that this tracker is only for issues. It is not for questions, comments, or feature requests.
|
|
||||||
|
|
||||||
If you are looking for support, please file an issue or email team@oxen.io.
|
|
||||||
|
|
||||||
Let's begin with a checklist: Replace the empty checkboxes [ ] below with checked ones [x] accordingly. -->
|
|
||||||
|
|
||||||
- [ ] I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
|
|
||||||
- [ ] I have searched open and closed issues for duplicates
|
|
||||||
- [ ] I am submitting a bug report for existing functionality that does not work as intended
|
|
||||||
- [ ] This isn't a feature request or a discussion topic
|
|
||||||
|
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
### Bug description
|
|
||||||
Describe here the issue that you are experiencing.
|
|
||||||
|
|
||||||
### Steps to reproduce
|
|
||||||
- using hyphens as bullet points
|
|
||||||
- list the steps
|
|
||||||
- that reproduce the bug
|
|
||||||
|
|
||||||
**Actual result:**
|
|
||||||
|
|
||||||
Describe here what happens after you run the steps above (i.e. the buggy behaviour)
|
|
||||||
|
|
||||||
**Expected result:**
|
|
||||||
|
|
||||||
Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour)
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
<!-- you can drag and drop images below -->
|
|
||||||
|
|
||||||
### Device info
|
|
||||||
<!-- replace the examples with your info -->
|
|
||||||
|
|
||||||
**Device:** Manufacturer Model XVI
|
|
||||||
|
|
||||||
**Android version:** 0.0.0
|
|
||||||
|
|
||||||
**Session version:** 0.0.0
|
|
||||||
|
|
||||||
### Link to debug log
|
|
|
@ -1,34 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Code of conduct**
|
|
||||||
|
|
||||||
- [ ] I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To reproduce**
|
|
||||||
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
**Screenshots or logs**
|
|
||||||
|
|
||||||
If applicable, add screenshots or logs to help explain your problem.
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
|
||||||
|
|
||||||
- Device: [e.g. Samsung Galaxy S8]
|
|
||||||
- OS: [e.g. Android Pie]
|
|
||||||
- Version of Session or latest commit hash
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
|
|
||||||
Add any other context about the problem here.
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
name: 🐞 Bug Report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
title: "[BUG] <title>"
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Code of conduct
|
||||||
|
description: I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
|
||||||
|
options:
|
||||||
|
- label: I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md)
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Self-training on how to write a bug report
|
||||||
|
description: High quality bug reports can help the team save time and improve the chance of getting your issue fixed. Please read [how to write a bug report](https://www.browserstack.com/guide/how-to-write-a-bug-report) before submitting your issue.
|
||||||
|
options:
|
||||||
|
- label: I have learned [how to write a bug report](https://www.browserstack.com/guide/how-to-write-a-bug-report)
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Is there an existing issue for this?
|
||||||
|
description: Please search to see if an issue already exists for the bug you encountered.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Current Behavior
|
||||||
|
description: A concise description of what you're experiencing.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: A concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps To Reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. In this environment...
|
||||||
|
2. With this config...
|
||||||
|
3. Run '...'
|
||||||
|
4. See error...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Android Version
|
||||||
|
description: What version of Android are you running?
|
||||||
|
placeholder: ex. Android 11
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Session Version
|
||||||
|
description: What version of Session are you running? (This can be found at the bottom of the app settings)
|
||||||
|
placeholder: ex. 1.17.0 (3425)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Anything else?
|
||||||
|
description: |
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
Tip: You can attach screenshots or log files to help explain your problem by clicking this area to highlight it and then dragging files in.
|
||||||
|
validations:
|
||||||
|
required: false
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: 🚀 Feature request
|
||||||
|
description: Suggest an idea for Session
|
||||||
|
title: '[Feature] <title>'
|
||||||
|
labels: [feature-request]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Is there an existing request for feature?
|
||||||
|
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: What feature would you like?
|
||||||
|
description: |
|
||||||
|
A clear and concise description of the feature you would like added to Session
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Anything else?
|
||||||
|
description: |
|
||||||
|
Add any other context or screenshots about the feature request here
|
||||||
|
validations:
|
||||||
|
required: false
|
|
@ -15,4 +15,5 @@ signing.properties
|
||||||
ffpr
|
ffpr
|
||||||
*.sh
|
*.sh
|
||||||
pkcs11.password
|
pkcs11.password
|
||||||
play
|
app/play
|
||||||
|
app/huawei
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "libsession-util/libsession-util"]
|
||||||
|
path = libsession-util/libsession-util
|
||||||
|
url = https://github.com/oxen-io/libsession-util.git
|
|
@ -32,6 +32,13 @@ Setting up a development environment and building from Android Studio
|
||||||
4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes".
|
4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes".
|
||||||
5. Default config options should be good enough.
|
5. Default config options should be good enough.
|
||||||
6. Project initialization and building should proceed.
|
6. Project initialization and building should proceed.
|
||||||
|
7. Clone submodules with `git submodule update --init --recursive`
|
||||||
|
|
||||||
|
If you would like to build the Huawei Flavor with Huawei HMS push notifications you will need to pass 'huawei' as a command line arg to include the required dependencies.
|
||||||
|
|
||||||
|
e.g. `./gradlew assembleHuaweiDebug -Phuawei`
|
||||||
|
|
||||||
|
If you are building in Android Studio then add `-Phuawei` to `Preferences > Build, Execution, Deployment > Gradle-Android Compiler > Command-line Options`
|
||||||
|
|
||||||
Contributing code
|
Contributing code
|
||||||
-----------------
|
-----------------
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
|
|
||||||
Add the [F-Droid repo](https://fdroid.getsession.org/)
|
Add the [F-Droid repo](https://fdroid.getsession.org/)
|
||||||
|
|
||||||
[Download the APK from here](https://github.com/loki-project/session-android/releases/latest)
|
[Download the APK from here](https://github.com/oxen-io/session-android/releases/latest)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Session integrates directly with [Oxen Service Nodes](https://docs.oxen.io/about-the-oxen-blockchain/oxen-service-nodes), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
|
Session integrates directly with [Oxen Service Nodes](https://docs.oxen.io/about-the-oxen-blockchain/oxen-service-nodes), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
|
||||||
|
|
||||||
<img src="https://i.imgur.com/dO9f7Hg.jpg" width="320" />
|
<img src="https://i.imgur.com/wcdAGBh.png" width="320" />
|
||||||
|
|
||||||
## Want to contribute? Found a bug or have a feature request?
|
## Want to contribute? Found a bug or have a feature request?
|
||||||
|
|
||||||
|
|
426
app/build.gradle
|
@ -1,24 +1,29 @@
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
||||||
classpath files('libs/gradle-witness.jar')
|
classpath files('libs/gradle-witness.jar')
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||||
classpath "com.google.gms:google-services:4.3.10"
|
classpath "com.google.gms:google-services:$googleServicesVersion"
|
||||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'kotlin-kapt'
|
||||||
|
id 'com.google.dagger.hilt.android'
|
||||||
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'witness'
|
apply plugin: 'witness'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-parcelize'
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
apply plugin: 'dagger.hilt.android.plugin'
|
apply plugin: 'dagger.hilt.android.plugin'
|
||||||
|
|
||||||
|
@ -26,31 +31,210 @@ configurations.all {
|
||||||
exclude module: "commons-logging"
|
exclude module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def canonicalVersionCode = 360
|
||||||
|
def canonicalVersionName = "1.17.5"
|
||||||
|
|
||||||
|
def postFixSize = 10
|
||||||
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
'arm64-v8a' : 2,
|
||||||
|
'x86' : 3,
|
||||||
|
'x86_64' : 4,
|
||||||
|
'universal' : 5]
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion androidCompileSdkVersion
|
||||||
|
namespace 'network.loki.messenger'
|
||||||
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'LICENSE.txt'
|
||||||
|
exclude 'LICENSE'
|
||||||
|
exclude 'NOTICE'
|
||||||
|
exclude 'asm-license.txt'
|
||||||
|
exclude 'META-INF/LICENSE'
|
||||||
|
exclude 'META-INF/NOTICE'
|
||||||
|
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||||
|
}
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
|
universalApk true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion '1.4.7'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
versionCode canonicalVersionCode * postFixSize
|
||||||
|
versionName canonicalVersionName
|
||||||
|
|
||||||
|
minSdkVersion androidMinimumSdkVersion
|
||||||
|
targetSdkVersion androidTargetSdkVersion
|
||||||
|
|
||||||
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
project.ext.set("archivesBaseName", "session")
|
||||||
|
|
||||||
|
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||||
|
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||||
|
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||||
|
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||||
|
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||||
|
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||||
|
|
||||||
|
resConfigs autoResConfig()
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
// The following argument makes the Android Test Orchestrator run its
|
||||||
|
// "pm clear" command after each test invocation. This command ensures
|
||||||
|
// that the app's state is completely cleared between tests.
|
||||||
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
testOptions {
|
||||||
|
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
String sharedTestDir = 'src/sharedTest/java'
|
||||||
|
test.java.srcDirs += sharedTestDir
|
||||||
|
androidTest.java.srcDirs += sharedTestDir
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
isDefault true
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions "distribution"
|
||||||
|
productFlavors {
|
||||||
|
play {
|
||||||
|
isDefault true
|
||||||
|
dimension "distribution"
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
ext.websiteUpdateUrl = "null"
|
||||||
|
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||||
|
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
|
||||||
|
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||||
|
buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"'
|
||||||
|
}
|
||||||
|
|
||||||
|
huawei {
|
||||||
|
dimension "distribution"
|
||||||
|
ext.websiteUpdateUrl = "null"
|
||||||
|
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||||
|
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.HUAWEI"
|
||||||
|
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||||
|
buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"_HUAWEI\"'
|
||||||
|
}
|
||||||
|
|
||||||
|
website {
|
||||||
|
dimension "distribution"
|
||||||
|
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
|
||||||
|
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||||
|
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
|
||||||
|
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||||
|
buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||||
|
def postFix = abiPostFix.get(abiName, 0)
|
||||||
|
|
||||||
|
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||||
|
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
|
||||||
|
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError true
|
||||||
|
baseline file("lint-baseline.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
dataBinding true
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
|
|
||||||
|
def huaweiEnabled = project.properties['huawei'] != null
|
||||||
|
|
||||||
|
applicationVariants.configureEach { variant ->
|
||||||
|
if (variant.flavorName == 'huawei') {
|
||||||
|
variant.getPreBuildProvider().configure { task ->
|
||||||
|
task.doFirst {
|
||||||
|
if (!huaweiEnabled) {
|
||||||
|
def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See BUILDING.md'
|
||||||
|
logger.error(message)
|
||||||
|
throw new GradleException(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation("com.google.dagger:hilt-android:2.46.1")
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
kapt("com.google.dagger:hilt-android-compiler:2.44")
|
||||||
|
|
||||||
|
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
|
implementation "com.google.android.material:material:$materialVersion"
|
||||||
|
implementation 'com.google.android:flexbox:2.0.1'
|
||||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||||
implementation 'androidx.activity:activity-ktx:1.2.2'
|
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.2'
|
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation 'androidx.activity:activity-ktx:1.5.1'
|
||||||
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||||
implementation ("com.google.firebase:firebase-messaging:18.0.0") {
|
implementation "androidx.core:core-ktx:$coreVersion"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||||
|
playImplementation ("com.google.firebase:firebase-messaging:18.0.0") {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||||
}
|
}
|
||||||
|
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||||
|
@ -90,23 +274,17 @@ dependencies {
|
||||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||||
}
|
}
|
||||||
implementation 'com.annimon:stream:1.1.8'
|
implementation 'com.annimon:stream:1.1.8'
|
||||||
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
|
|
||||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
|
||||||
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
|
||||||
}
|
|
||||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
implementation 'androidx.sqlite:sqlite-ktx:2.3.1'
|
||||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'
|
||||||
exclude group: 'com.fasterxml.jackson.core'
|
|
||||||
exclude group: 'org.freemarker'
|
|
||||||
}
|
|
||||||
implementation project(":libsignal")
|
implementation project(":libsignal")
|
||||||
implementation project(":libsession")
|
implementation project(":libsession")
|
||||||
|
implementation project(":libsession-util")
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||||
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
|
||||||
implementation 'com.goterl:lazysodium-android:5.0.2@aar'
|
implementation project(":liblazysodium")
|
||||||
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
implementation "net.java.dev.jna:jna:5.12.1@aar"
|
||||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
|
@ -115,183 +293,58 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||||
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
||||||
implementation "com.github.lelloman:android-identicons:v11"
|
|
||||||
implementation "com.prof.rssparser:rssparser:2.0.4"
|
|
||||||
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
|
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
|
||||||
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
|
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
|
||||||
implementation "com.github.ybq:Android-SpinKit:1.4.0"
|
implementation "com.github.ybq:Android-SpinKit:1.4.0"
|
||||||
implementation "com.opencsv:opencsv:4.6"
|
implementation "com.opencsv:opencsv:4.6"
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation "junit:junit:$junitVersion"
|
||||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||||
testImplementation "org.mockito:mockito-inline:4.0.0"
|
testImplementation "org.mockito:mockito-inline:4.10.0"
|
||||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
androidTestImplementation "org.mockito:mockito-android:4.10.0"
|
||||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
testImplementation "androidx.test:core:$testCoreVersion"
|
||||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
testImplementation "androidx.arch.core:core-testing:2.2.0"
|
||||||
testImplementation 'androidx.test:core:1.3.0'
|
|
||||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||||
// Core library
|
// Core library
|
||||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
androidTestImplementation "androidx.test:core:$testCoreVersion"
|
||||||
|
|
||||||
|
androidTestImplementation('com.adevinta.android:barista:4.2.0') {
|
||||||
|
exclude group: 'org.jetbrains.kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
// AndroidJUnitRunner and JUnit Rules
|
// AndroidJUnitRunner and JUnit Rules
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
|
|
||||||
// Assertions
|
// Assertions
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.ext:truth:1.4.0'
|
androidTestImplementation 'androidx.test.ext:truth:1.5.0'
|
||||||
androidTestImplementation 'com.google.truth:truth:1.0'
|
androidTestImplementation 'com.google.truth:truth:1.1.3'
|
||||||
|
|
||||||
// Espresso dependencies
|
// Espresso dependencies
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.5.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
|
||||||
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
|
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
|
||||||
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||||
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.4'
|
testImplementation 'org.robolectric:robolectric:4.4'
|
||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
}
|
|
||||||
|
|
||||||
def canonicalVersionCode = 286
|
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
||||||
def canonicalVersionName = "1.13.5"
|
implementation 'androidx.compose.ui:ui:1.5.2'
|
||||||
|
implementation 'androidx.compose.ui:ui-tooling:1.5.2'
|
||||||
|
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
||||||
|
implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
|
||||||
|
implementation "androidx.compose.runtime:runtime-livedata:1.5.2"
|
||||||
|
|
||||||
def postFixSize = 10
|
implementation 'androidx.compose.foundation:foundation-layout:1.5.2'
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
implementation 'androidx.compose.material:material:1.5.2'
|
||||||
'arm64-v8a' : 2,
|
|
||||||
'x86' : 3,
|
|
||||||
'x86_64' : 4,
|
|
||||||
'universal' : 5]
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion androidCompileSdkVersion
|
|
||||||
buildToolsVersion '29.0.3'
|
|
||||||
useLibrary 'org.apache.http.legacy'
|
|
||||||
|
|
||||||
dexOptions {
|
|
||||||
javaMaxHeapSize "4g"
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude 'LICENSE.txt'
|
|
||||||
exclude 'LICENSE'
|
|
||||||
exclude 'NOTICE'
|
|
||||||
exclude 'asm-license.txt'
|
|
||||||
exclude 'META-INF/LICENSE'
|
|
||||||
exclude 'META-INF/NOTICE'
|
|
||||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
|
||||||
}
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
enable true
|
|
||||||
reset()
|
|
||||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
|
||||||
universalApk true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
versionCode canonicalVersionCode * postFixSize
|
|
||||||
versionName canonicalVersionName
|
|
||||||
|
|
||||||
minSdkVersion androidMinimumSdkVersion
|
|
||||||
targetSdkVersion androidCompileSdkVersion
|
|
||||||
|
|
||||||
multiDexEnabled = true
|
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
project.ext.set("archivesBaseName", "session")
|
|
||||||
|
|
||||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
|
||||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
|
||||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
|
||||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
|
||||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
|
||||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
|
||||||
|
|
||||||
resConfigs autoResConfig()
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
// The following argument makes the Android Test Orchestrator run its
|
|
||||||
// "pm clear" command after each test invocation. This command ensures
|
|
||||||
// that the app's state is completely cleared between tests.
|
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
|
||||||
testOptions {
|
|
||||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
String sharedTestDir = 'src/sharedTest/java'
|
|
||||||
test.java.srcDirs += sharedTestDir
|
|
||||||
androidTest.java.srcDirs += sharedTestDir
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
minifyEnabled false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "distribution"
|
|
||||||
productFlavors {
|
|
||||||
play {
|
|
||||||
ext.websiteUpdateUrl = "null"
|
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
|
||||||
}
|
|
||||||
|
|
||||||
website {
|
|
||||||
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
|
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationVariants.all { variant ->
|
|
||||||
variant.outputs.each { output ->
|
|
||||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
|
||||||
def postFix = abiPostFix.get(abiName, 0)
|
|
||||||
|
|
||||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
|
||||||
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
|
|
||||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
abortOnError true
|
|
||||||
baseline file("lint-baseline.xml")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests {
|
|
||||||
includeAndroidResources = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
dataBinding true
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static def getLastCommitTimestamp() {
|
static def getLastCommitTimestamp() {
|
||||||
|
@ -312,3 +365,8 @@ def autoResConfig() {
|
||||||
.collect { matcher -> matcher.group(1) }
|
.collect { matcher -> matcher.group(1) }
|
||||||
.sort()
|
.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow references to generated code
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
-keep class org.whispersystems.** { *; }
|
-keep class org.whispersystems.** { *; }
|
||||||
-keep class org.thoughtcrime.securesms.** { *; }
|
-keep class org.thoughtcrime.securesms.** { *; }
|
||||||
|
-keep class org.thoughtcrime.securesms.components.menu.** { *; }
|
||||||
-keep class org.session.** { *; }
|
-keep class org.session.** { *; }
|
||||||
-keepclassmembers class ** {
|
-keepclassmembers class ** {
|
||||||
public void onEvent*(**);
|
public void onEvent*(**);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package network.loki.messenger
|
package network.loki.messenger
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Instrumentation
|
import android.app.Instrumentation
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -21,8 +22,8 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.adevinta.android.barista.interaction.PermissionGranter
|
||||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||||
import network.loki.messenger.util.NewConversationButtonDrawableMatcher.Companion.newConversationButtonWithDrawable
|
|
||||||
import org.hamcrest.Matcher
|
import org.hamcrest.Matcher
|
||||||
import org.hamcrest.Matchers.allOf
|
import org.hamcrest.Matchers.allOf
|
||||||
import org.hamcrest.Matchers.not
|
import org.hamcrest.Matchers.not
|
||||||
|
@ -39,7 +40,6 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
import org.thoughtcrime.securesms.home.HomeActivity
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
class HomeActivityTests {
|
class HomeActivityTests {
|
||||||
|
@ -87,11 +87,13 @@ class HomeActivityTests {
|
||||||
}
|
}
|
||||||
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
|
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
||||||
|
// allow notification permission
|
||||||
|
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun goToMyChat() {
|
private fun goToMyChat() {
|
||||||
onView(newConversationButtonWithDrawable(R.drawable.ic_plus)).perform(ViewActions.click())
|
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
||||||
onView(newConversationButtonWithDrawable(R.drawable.ic_message)).perform(ViewActions.click())
|
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||||
// new chat
|
// new chat
|
||||||
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
|
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
|
||||||
onView(withId(R.id.copyButton)).perform(ViewActions.click())
|
onView(withId(R.id.copyButton)).perform(ViewActions.click())
|
||||||
|
@ -102,6 +104,7 @@ class HomeActivityTests {
|
||||||
copied = clipboardManager.primaryClip!!.getItemAt(0).text.toString()
|
copied = clipboardManager.primaryClip!!.getItemAt(0).text.toString()
|
||||||
}
|
}
|
||||||
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied))
|
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied))
|
||||||
|
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
|
||||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +158,7 @@ class HomeActivityTests {
|
||||||
|
|
||||||
val dialogPromptText = InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.dialog_open_url_explanation, amazonPuny)
|
val dialogPromptText = InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.dialog_open_url_explanation, amazonPuny)
|
||||||
|
|
||||||
|
onView(isRoot()).perform(waitFor(1000)) // no other way for this to work apparently
|
||||||
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
package network.loki.messenger
|
||||||
|
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import network.loki.messenger.libsession_util.ConfigBase
|
||||||
|
import network.loki.messenger.libsession_util.Contacts
|
||||||
|
import network.loki.messenger.libsession_util.util.Contact
|
||||||
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.kotlin.argThat
|
||||||
|
import org.mockito.kotlin.eq
|
||||||
|
import org.mockito.kotlin.spy
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsignal.utilities.KeyHelper
|
||||||
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@SmallTest
|
||||||
|
class LibSessionTests {
|
||||||
|
|
||||||
|
private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||||
|
private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
|
||||||
|
private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
|
||||||
|
|
||||||
|
private var fakeHashI = 0
|
||||||
|
private val nextFakeHash: String
|
||||||
|
get() = "fakehash${fakeHashI++}"
|
||||||
|
|
||||||
|
private fun maybeGetUserInfo(): Pair<ByteArray, String>? {
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||||
|
val prefs = appContext.prefs
|
||||||
|
val localUserPublicKey = prefs.getLocalNumber()
|
||||||
|
val secretKey = with(appContext) {
|
||||||
|
val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
|
||||||
|
edKey.secretKey.asBytes
|
||||||
|
}
|
||||||
|
return if (localUserPublicKey == null || secretKey == null) null
|
||||||
|
else secretKey to localUserPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildContactMessage(contactList: List<Contact>): ByteArray {
|
||||||
|
val (key,_) = maybeGetUserInfo()!!
|
||||||
|
val contacts = Contacts.Companion.newInstance(key)
|
||||||
|
contactList.forEach { contact ->
|
||||||
|
contacts.set(contact)
|
||||||
|
}
|
||||||
|
return contacts.push().config
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fakePollNewConfig(configBase: ConfigBase, toMerge: ByteArray) {
|
||||||
|
configBase.merge(nextFakeHash to toMerge)
|
||||||
|
MessagingModuleConfiguration.shared.configFactory.persist(configBase, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setupUser() {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext).edit {
|
||||||
|
putBoolean(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, true).apply()
|
||||||
|
}
|
||||||
|
val newBytes = randomSeedBytes().toByteArray()
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||||
|
val kp = KeyPairUtilities.generate(newBytes)
|
||||||
|
KeyPairUtilities.store(context, kp.seed, kp.ed25519KeyPair, kp.x25519KeyPair)
|
||||||
|
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||||
|
TextSecurePreferences.setLocalRegistrationId(context, registrationID)
|
||||||
|
TextSecurePreferences.setLocalNumber(context, kp.x25519KeyPair.hexEncodedPublicKey)
|
||||||
|
TextSecurePreferences.setRestorationTime(context, 0)
|
||||||
|
TextSecurePreferences.setHasViewedSeed(context, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migration_one_to_ones() {
|
||||||
|
val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||||
|
val storageSpy = spy(app.storage)
|
||||||
|
app.storage = storageSpy
|
||||||
|
|
||||||
|
val newContactId = randomSessionId()
|
||||||
|
val singleContact = Contact(
|
||||||
|
id = newContactId,
|
||||||
|
approved = true,
|
||||||
|
expiryMode = ExpiryMode.NONE
|
||||||
|
)
|
||||||
|
val newContactMerge = buildContactMessage(listOf(singleContact))
|
||||||
|
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
|
||||||
|
fakePollNewConfig(contacts, newContactMerge)
|
||||||
|
verify(storageSpy).addLibSessionContacts(argThat {
|
||||||
|
first().let { it.id == newContactId && it.approved } && size == 1
|
||||||
|
})
|
||||||
|
verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
package network.loki.messenger
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.goterl.lazysodium.utils.Key
|
||||||
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SodiumUtilitiesTest {
|
||||||
|
|
||||||
|
private val publicKey: String = "88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"
|
||||||
|
private val privateKey: String = "30d796c1ddb4dc455fd998a98aa275c247494a9a7bde9c1fee86ae45cd585241"
|
||||||
|
private val edKeySeed: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9"
|
||||||
|
private val edPublicKey: String = "bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
|
||||||
|
private val edSecretKey: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
|
||||||
|
private val blindedPublicKey: String = "98932d4bccbe595a8789d7eb1629cefc483a0eaddc7e20e8fe5c771efafd9af5"
|
||||||
|
private val serverPublicKey: String = "c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d"
|
||||||
|
|
||||||
|
private val edKeyPair = KeyPair(Key.fromHexString(edPublicKey), Key.fromHexString(edSecretKey))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun generateBlindingFactorSuccess() {
|
||||||
|
val result = SodiumUtilities.generateBlindingFactor(serverPublicKey)
|
||||||
|
|
||||||
|
assertThat(result?.toHexString(), equalTo("84e3eb75028a9b73fec031b7448e322a68ca6485fad81ab1bead56f759ebeb0f"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun generateBlindingFactorFailure() {
|
||||||
|
val result = SodiumUtilities.generateBlindingFactor("Test")
|
||||||
|
|
||||||
|
assertNull(result?.toHexString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blindedKeyPairSuccess() {
|
||||||
|
val result = SodiumUtilities.blindedKeyPair(serverPublicKey, edKeyPair)!!
|
||||||
|
|
||||||
|
assertThat(result.publicKey.asHexString.lowercase(), equalTo(blindedPublicKey))
|
||||||
|
assertThat(result.secretKey.asHexString.take(64).lowercase(), equalTo("16663322d6b684e1c9dcc02b9e8642c3affd3bc431a9ea9e63dbbac88ce7a305"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blindedKeyPairFailurePublicKeyLength() {
|
||||||
|
val result = SodiumUtilities.blindedKeyPair(
|
||||||
|
serverPublicKey,
|
||||||
|
KeyPair(Key.fromHexString(edPublicKey.take(4)), Key.fromHexString(edKeySeed))
|
||||||
|
)
|
||||||
|
|
||||||
|
assertNull(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blindedKeyPairFailureSecretKeyLength() {
|
||||||
|
val result = SodiumUtilities.blindedKeyPair(
|
||||||
|
serverPublicKey,
|
||||||
|
KeyPair(Key.fromHexString(edPublicKey), Key.fromHexString(edSecretKey.take(4)))
|
||||||
|
)
|
||||||
|
|
||||||
|
assertNull(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blindedKeyPairFailureBlindingFactor() {
|
||||||
|
val result = SodiumUtilities.blindedKeyPair("Test", edKeyPair)
|
||||||
|
|
||||||
|
assertNull(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sogsSignature() {
|
||||||
|
val expectedSignature = "dcc086abdd2a740d9260b008fb37e12aa0ff47bd2bd9e177bbbec37fd46705a9072ce747bda66c788c3775cdd7ad60ad15a478e0886779aad5d795fd7bf8350d"
|
||||||
|
|
||||||
|
val result = SodiumUtilities.sogsSignature(
|
||||||
|
"TestMessage".toByteArray(),
|
||||||
|
Hex.fromStringCondensed(edSecretKey),
|
||||||
|
Hex.fromStringCondensed("44d82cc15c0a5056825cae7520b6b52d000a23eb0c5ed94c4be2d9dc41d2d409"),
|
||||||
|
Hex.fromStringCondensed("0bb7815abb6ba5142865895f3e5286c0527ba4d31dbb75c53ce95e91ffe025a2")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(result?.toHexString(), equalTo(expectedSignature))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun combineKeysSuccess() {
|
||||||
|
val result = SodiumUtilities.combineKeys(
|
||||||
|
Hex.fromStringCondensed(edSecretKey),
|
||||||
|
Hex.fromStringCondensed(edPublicKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(result?.toHexString(), equalTo("1159b5d0fcfba21228eb2121a0f59712fa8276fc6e5547ff519685a40b9819e6"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun combineKeysFailure() {
|
||||||
|
val result = SodiumUtilities.combineKeys(
|
||||||
|
SodiumUtilities.generatePrivateKeyScalar(Hex.fromStringCondensed(edSecretKey))!!,
|
||||||
|
Hex.fromStringCondensed(publicKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertNull(result?.toHexString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sharedBlindedEncryptionKeySuccess() {
|
||||||
|
val result = SodiumUtilities.sharedBlindedEncryptionKey(
|
||||||
|
Hex.fromStringCondensed(edSecretKey),
|
||||||
|
Hex.fromStringCondensed(blindedPublicKey),
|
||||||
|
Hex.fromStringCondensed(publicKey),
|
||||||
|
Hex.fromStringCondensed(blindedPublicKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(result?.toHexString(), equalTo("388ee09e4c356b91f1cce5cc0aa0cf59e8e8cade69af61685d09c2d2731bc99e"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sharedBlindedEncryptionKeyFailure() {
|
||||||
|
val result = SodiumUtilities.sharedBlindedEncryptionKey(
|
||||||
|
Hex.fromStringCondensed(edSecretKey),
|
||||||
|
Hex.fromStringCondensed(publicKey),
|
||||||
|
Hex.fromStringCondensed(edPublicKey),
|
||||||
|
Hex.fromStringCondensed(publicKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertNull(result?.toHexString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionIdSuccess() {
|
||||||
|
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
|
||||||
|
|
||||||
|
assertTrue(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionIdFailureInvalidSessionId() {
|
||||||
|
val result = SodiumUtilities.sessionId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
|
||||||
|
|
||||||
|
assertFalse(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionIdFailureInvalidBlindedId() {
|
||||||
|
val result = SodiumUtilities.sessionId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
|
||||||
|
|
||||||
|
assertFalse(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionIdFailureBlindingFactor() {
|
||||||
|
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", "Test")
|
||||||
|
|
||||||
|
assertFalse(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,24 +5,6 @@ import androidx.annotation.DrawableRes
|
||||||
import org.hamcrest.Description
|
import org.hamcrest.Description
|
||||||
import org.hamcrest.TypeSafeMatcher
|
import org.hamcrest.TypeSafeMatcher
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
||||||
import org.thoughtcrime.securesms.home.NewConversationButtonSetView
|
|
||||||
|
|
||||||
class NewConversationButtonDrawableMatcher(@DrawableRes private val expectedId: Int): TypeSafeMatcher<View>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic fun newConversationButtonWithDrawable(@DrawableRes expectedId: Int) = NewConversationButtonDrawableMatcher(expectedId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeTo(description: Description?) {
|
|
||||||
description?.appendText("with drawable on button with resource id: $expectedId")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun matchesSafely(item: View): Boolean {
|
|
||||||
if (item !is NewConversationButtonSetView.Button) return false
|
|
||||||
|
|
||||||
return item.getIconID() == expectedId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InputBarButtonDrawableMatcher(@DrawableRes private val expectedId: Int): TypeSafeMatcher<View>() {
|
class InputBarButtonDrawableMatcher(@DrawableRes private val expectedId: Int): TypeSafeMatcher<View>() {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="com.huawei.hms.client.appid"
|
||||||
|
android:value="appid=107205081">
|
||||||
|
</meta-data>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.huawei.hms.client.cpid"
|
||||||
|
android:value="cpid=30061000024605000">
|
||||||
|
</meta-data>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="org.thoughtcrime.securesms.notifications.HuaweiPushService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"agcgw":{
|
||||||
|
"backurl":"connect-dre.hispace.hicloud.com",
|
||||||
|
"url":"connect-dre.dbankcloud.cn",
|
||||||
|
"websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com",
|
||||||
|
"websocketurl":"connect-ws-dre.hispace.dbankcloud.cn"
|
||||||
|
},
|
||||||
|
"agcgw_all":{
|
||||||
|
"CN":"connect-drcn.dbankcloud.cn",
|
||||||
|
"CN_back":"connect-drcn.hispace.hicloud.com",
|
||||||
|
"DE":"connect-dre.dbankcloud.cn",
|
||||||
|
"DE_back":"connect-dre.hispace.hicloud.com",
|
||||||
|
"RU":"connect-drru.hispace.dbankcloud.ru",
|
||||||
|
"RU_back":"connect-drru.hispace.dbankcloud.cn",
|
||||||
|
"SG":"connect-dra.dbankcloud.cn",
|
||||||
|
"SG_back":"connect-dra.hispace.hicloud.com"
|
||||||
|
},
|
||||||
|
"websocketgw_all":{
|
||||||
|
"CN":"connect-ws-drcn.hispace.dbankcloud.cn",
|
||||||
|
"CN_back":"connect-ws-drcn.hispace.dbankcloud.com",
|
||||||
|
"DE":"connect-ws-dre.hispace.dbankcloud.cn",
|
||||||
|
"DE_back":"connect-ws-dre.hispace.dbankcloud.com",
|
||||||
|
"RU":"connect-ws-drru.hispace.dbankcloud.ru",
|
||||||
|
"RU_back":"connect-ws-drru.hispace.dbankcloud.cn",
|
||||||
|
"SG":"connect-ws-dra.hispace.dbankcloud.cn",
|
||||||
|
"SG_back":"connect-ws-dra.hispace.dbankcloud.com"
|
||||||
|
},
|
||||||
|
"client":{
|
||||||
|
"cp_id":"890061000023000573",
|
||||||
|
"product_id":"99536292102532562",
|
||||||
|
"client_id":"954244311350791232",
|
||||||
|
"client_secret":"555999202D718B6744DAD2E923B386DC17F3F4E29F5105CE0D061EED328DADEE",
|
||||||
|
"project_id":"99536292102532562",
|
||||||
|
"app_id":"107205081",
|
||||||
|
"api_key":"DAEDABeddLEqUy0QRwa1THLwRA0OqrSuyci/HjNvVSmsdWsXRM2U2hRaCyqfvGYH1IFOKrauArssz/WPMLRHCYxliWf+DTj9bDwlWA==",
|
||||||
|
"package_name":"network.loki.messenger"
|
||||||
|
},
|
||||||
|
"oauth_client":{
|
||||||
|
"client_id":"107205081",
|
||||||
|
"client_type":1
|
||||||
|
},
|
||||||
|
"app_info":{
|
||||||
|
"app_id":"107205081",
|
||||||
|
"package_name":"network.loki.messenger"
|
||||||
|
},
|
||||||
|
"service":{
|
||||||
|
"analytics":{
|
||||||
|
"collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||||
|
"collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||||
|
"collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
||||||
|
"collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||||
|
"collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
||||||
|
"resource_id":"p1",
|
||||||
|
"channel_id":""
|
||||||
|
},
|
||||||
|
"edukit":{
|
||||||
|
"edu_url":"edukit.edu.cloud.huawei.com.cn",
|
||||||
|
"dh_url":"edukit.edu.cloud.huawei.com.cn"
|
||||||
|
},
|
||||||
|
"search":{
|
||||||
|
"url":"https://search-dre.cloud.huawei.com"
|
||||||
|
},
|
||||||
|
"cloudstorage":{
|
||||||
|
"storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia",
|
||||||
|
"storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru",
|
||||||
|
"storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru",
|
||||||
|
"storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu",
|
||||||
|
"storage_url_de":"https://ops-dre.agcstorage.link",
|
||||||
|
"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||||
|
"storage_url_sg":"https://ops-dra.agcstorage.link",
|
||||||
|
"storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn",
|
||||||
|
"storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn"
|
||||||
|
},
|
||||||
|
"ml":{
|
||||||
|
"mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"region":"DE",
|
||||||
|
"configuration_version":"3.0",
|
||||||
|
"appInfos":[
|
||||||
|
{
|
||||||
|
"package_name":"network.loki.messenger",
|
||||||
|
"client":{
|
||||||
|
"app_id":"107205081"
|
||||||
|
},
|
||||||
|
"app_info":{
|
||||||
|
"package_name":"network.loki.messenger",
|
||||||
|
"app_id":"107205081"
|
||||||
|
},
|
||||||
|
"oauth_client":{
|
||||||
|
"client_type":1,
|
||||||
|
"client_id":"107205081"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.thoughtcrime.securesms.notifications
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class HuaweiBindingModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun bindTokenFetcher(tokenFetcher: HuaweiTokenFetcher): TokenFetcher
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.thoughtcrime.securesms.notifications
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.huawei.hms.push.HmsMessageService
|
||||||
|
import com.huawei.hms.push.RemoteMessage
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import java.lang.Exception
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val TAG = HuaweiPushService::class.java.simpleName
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class HuaweiPushService: HmsMessageService() {
|
||||||
|
@Inject lateinit var pushRegistry: PushRegistry
|
||||||
|
@Inject lateinit var pushReceiver: PushReceiver
|
||||||
|
|
||||||
|
override fun onMessageReceived(message: RemoteMessage?) {
|
||||||
|
Log.d(TAG, "onMessageReceived")
|
||||||
|
message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPush) ?:
|
||||||
|
pushReceiver.onPush(message?.data?.let(Base64::decode))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewToken(token: String?) {
|
||||||
|
pushRegistry.register(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewToken(token: String?, bundle: Bundle?) {
|
||||||
|
Log.d(TAG, "New HCM token: $token.")
|
||||||
|
pushRegistry.register(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeletedMessages() {
|
||||||
|
Log.d(TAG, "onDeletedMessages")
|
||||||
|
pushRegistry.refresh(false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.thoughtcrime.securesms.notifications
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.huawei.hms.aaid.HmsInstanceId
|
||||||
|
import dagger.Lazy
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
private const val APP_ID = "107205081"
|
||||||
|
private const val TOKEN_SCOPE = "HCM"
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class HuaweiTokenFetcher @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val pushRegistry: Lazy<PushRegistry>,
|
||||||
|
): TokenFetcher {
|
||||||
|
override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run {
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/push-basic-capability#h2-1576218800370
|
||||||
|
// getToken may return an empty string, if so HuaweiPushService#onNewToken will be called.
|
||||||
|
withContext(Dispatchers.IO) { getToken(APP_ID, TOKEN_SCOPE) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="preferences_notifications_strategy_category_fast_mode_summary">You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers.</string>
|
||||||
|
<string name="activity_pn_mode_fast_mode_explanation">You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers.</string>
|
||||||
|
</resources>
|
|
@ -1,8 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="network.loki.messenger">
|
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" />
|
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" />
|
||||||
|
|
||||||
|
@ -30,11 +29,18 @@
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||||
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
|
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
@ -53,7 +59,6 @@
|
||||||
<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" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
|
||||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
|
@ -142,22 +147,15 @@
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.dms.CreatePrivateChatActivity"
|
android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:label="@string/blocked_contacts_title"
|
||||||
<activity
|
/>
|
||||||
android:name="org.thoughtcrime.securesms.groups.CreateClosedGroupActivity"
|
|
||||||
android:screenOrientation="portrait" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
|
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
|
||||||
android:label="@string/activity_edit_closed_group_title"
|
android:label="@string/activity_edit_closed_group_title"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.groups.JoinPublicChatActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.SeedActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.SeedActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
|
@ -174,8 +172,15 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.preferences.ChatSettingsActivity"
|
android:name="org.thoughtcrime.securesms.preferences.ChatSettingsActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity"
|
||||||
|
android:label="@string/activity_help_settings_title"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity"
|
||||||
|
android:screenOrientation="portrait"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="true"
|
||||||
android:name="org.thoughtcrime.securesms.ShareActivity"
|
android:name="org.thoughtcrime.securesms.ShareActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
@ -222,7 +227,7 @@
|
||||||
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
|
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar">
|
android:theme="@style/Theme.Session.DayNight.NoActionBar">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||||
|
@ -230,16 +235,8 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
|
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.TextSecure.DayNight">
|
android:theme="@style/Theme.Session.DayNight">
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.TextSecure.DayNight" />
|
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.TextSecure.DayNight" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.DatabaseUpgradeActivity"
|
android:name="org.thoughtcrime.securesms.DatabaseUpgradeActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
@ -253,14 +250,14 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.giph.ui.GiphyActivity"
|
android:name="org.thoughtcrime.securesms.giph.ui.GiphyActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:theme="@style/Theme.TextSecure.DayNight.NoActionBar"
|
android:theme="@style/Theme.Session.DayNight.NoActionBar"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="stateHidden" />
|
android:windowSoftInputMode="stateHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.mediasend.MediaSendActivity"
|
android:name="org.thoughtcrime.securesms.mediasend.MediaSendActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.TextSecure.DayNight.NoActionBar"
|
android:theme="@style/Theme.Session.DayNight.NoActionBar"
|
||||||
android:windowSoftInputMode="stateHidden" />
|
android:windowSoftInputMode="stateHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.MediaPreviewActivity"
|
android:name="org.thoughtcrime.securesms.MediaPreviewActivity"
|
||||||
|
@ -311,22 +308,19 @@
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<service
|
|
||||||
android:name="org.thoughtcrime.securesms.notifications.PushNotificationService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
|
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
|
||||||
|
android:foregroundServiceType="microphone"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="org.thoughtcrime.securesms.service.KeyCachingService"
|
android:name="org.thoughtcrime.securesms.service.KeyCachingService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" android:foregroundServiceType="specialUse">
|
||||||
|
<!-- <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"-->
|
||||||
|
<!-- android:value="@string/preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint"/>-->
|
||||||
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="org.thoughtcrime.securesms.service.DirectShareService"
|
android:name="org.thoughtcrime.securesms.service.DirectShareService"
|
||||||
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
|
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.chooser.ChooserTargetService" />
|
<action android:name="android.service.chooser.ChooserTargetService" />
|
||||||
|
@ -399,58 +393,54 @@
|
||||||
android:name="org.thoughtcrime.securesms.database.DatabaseContentProviders$StickerPack"
|
android:name="org.thoughtcrime.securesms.database.DatabaseContentProviders$StickerPack"
|
||||||
android:authorities="network.loki.securesms.database.stickerpack"
|
android:authorities="network.loki.securesms.database.stickerpack"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<provider
|
||||||
|
android:name="org.thoughtcrime.securesms.database.DatabaseContentProviders$Recipient"
|
||||||
|
android:authorities="network.loki.securesms.database.recipient"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name="org.thoughtcrime.securesms.service.BootReceiver">
|
<receiver android:name="org.thoughtcrime.securesms.service.BootReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="network.loki.securesms.RESTART" />
|
<action android:name="network.loki.securesms.RESTART" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener">
|
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener">
|
<receiver android:name="org.thoughtcrime.securesms.notifications.LocaleChangedReceiver"
|
||||||
<intent-filter>
|
android:exported="true">
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name="org.thoughtcrime.securesms.notifications.LocaleChangedReceiver">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name="org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver">
|
<receiver android:name="org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="network.loki.securesms.DELETE_NOTIFICATION" />
|
<action android:name="network.loki.securesms.DELETE_NOTIFICATION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.thoughtcrime.securesms.service.PanicResponderListener"
|
android:name="org.thoughtcrime.securesms.service.PanicResponderListener"
|
||||||
android:exported="true">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver"
|
android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver"
|
||||||
android:enabled="true">
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<service
|
|
||||||
android:name="org.thoughtcrime.securesms.jobmanager.JobSchedulerScheduler$SystemService"
|
|
||||||
android:enabled="@bool/enable_job_service"
|
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
|
||||||
tools:targetApi="26" />
|
|
||||||
<service
|
<service
|
||||||
android:name="org.thoughtcrime.securesms.jobmanager.KeepAliveService"
|
android:name="org.thoughtcrime.securesms.jobmanager.KeepAliveService"
|
||||||
android:enabled="@bool/enable_alarm_manager" />
|
android:enabled="@bool/enable_alarm_manager" />
|
||||||
<receiver
|
|
||||||
android:name="org.thoughtcrime.securesms.jobmanager.AlarmManagerScheduler$RetryReceiver"
|
|
||||||
android:enabled="@bool/enable_alarm_manager" /> <!-- Probably don't need this one -->
|
|
||||||
<uses-library
|
<uses-library
|
||||||
android:name="com.sec.android.app.multiwindow"
|
android:name="com.sec.android.app.multiwindow"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 166 KiB |
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 176 KiB |
After Width: | Height: | Size: 99 KiB |
After Width: | Height: | Size: 150 KiB |
After Width: | Height: | Size: 149 KiB |
After Width: | Height: | Size: 150 KiB |
After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 174 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 200 KiB |
After Width: | Height: | Size: 91 KiB |
|
@ -1,8 +1,8 @@
|
||||||
package org.thoughtcrime.securesms
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import nl.komponents.kovenant.Kovenant
|
import nl.komponents.kovenant.Kovenant
|
||||||
import nl.komponents.kovenant.jvm.asDispatcher
|
import nl.komponents.kovenant.jvm.asDispatcher
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,8 @@ import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPol
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||||
import org.session.libsession.snode.SnodeModule;
|
import org.session.libsession.snode.SnodeModule;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
|
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
|
||||||
|
import org.session.libsession.utilities.Device;
|
||||||
import org.session.libsession.utilities.ProfilePictureUtilities;
|
import org.session.libsession.utilities.ProfilePictureUtilities;
|
||||||
import org.session.libsession.utilities.SSKEnvironment;
|
import org.session.libsession.utilities.SSKEnvironment;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
@ -47,40 +49,41 @@ import org.session.libsession.utilities.Util;
|
||||||
import org.session.libsession.utilities.WindowDebouncer;
|
import org.session.libsession.utilities.WindowDebouncer;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||||
|
import org.session.libsignal.utilities.HTTP;
|
||||||
|
import org.session.libsignal.utilities.JsonUtil;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.session.libsignal.utilities.ThreadUtils;
|
import org.session.libsignal.utilities.ThreadUtils;
|
||||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
|
||||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||||
import org.thoughtcrime.securesms.database.Storage;
|
import org.thoughtcrime.securesms.database.Storage;
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppComponent;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
|
||||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
|
||||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
|
||||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
||||||
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
|
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
|
||||||
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
||||||
import org.thoughtcrime.securesms.notifications.FcmUtils;
|
|
||||||
import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager;
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
||||||
|
import org.thoughtcrime.securesms.notifications.PushRegistry;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
|
||||||
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
|
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
|
||||||
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
|
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
|
||||||
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
|
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
|
||||||
import org.thoughtcrime.securesms.util.Broadcaster;
|
import org.thoughtcrime.securesms.util.Broadcaster;
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities;
|
|
||||||
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
|
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
|
||||||
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
|
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
|
||||||
import org.webrtc.PeerConnectionFactory;
|
import org.webrtc.PeerConnectionFactory;
|
||||||
|
@ -89,12 +92,16 @@ import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ -103,6 +110,8 @@ import dagger.hilt.android.HiltAndroidApp;
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import kotlinx.coroutines.Job;
|
import kotlinx.coroutines.Job;
|
||||||
import network.loki.messenger.BuildConfig;
|
import network.loki.messenger.BuildConfig;
|
||||||
|
import network.loki.messenger.libsession_util.ConfigBase;
|
||||||
|
import network.loki.messenger.libsession_util.UserProfile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will be called once when the TextSecure process is created.
|
* Will be called once when the TextSecure process is created.
|
||||||
|
@ -113,7 +122,7 @@ import network.loki.messenger.BuildConfig;
|
||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*/
|
*/
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
public class ApplicationContext extends Application implements DefaultLifecycleObserver {
|
public class ApplicationContext extends Application implements DefaultLifecycleObserver, ConfigFactoryUpdateListener {
|
||||||
|
|
||||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||||
|
|
||||||
|
@ -122,7 +131,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
private ExpiringMessageManager expiringMessageManager;
|
private ExpiringMessageManager expiringMessageManager;
|
||||||
private TypingStatusRepository typingStatusRepository;
|
private TypingStatusRepository typingStatusRepository;
|
||||||
private TypingStatusSender typingStatusSender;
|
private TypingStatusSender typingStatusSender;
|
||||||
private JobManager jobManager;
|
|
||||||
private ReadReceiptManager readReceiptManager;
|
private ReadReceiptManager readReceiptManager;
|
||||||
private ProfileManager profileManager;
|
private ProfileManager profileManager;
|
||||||
public MessageNotifier messageNotifier = null;
|
public MessageNotifier messageNotifier = null;
|
||||||
|
@ -135,10 +143,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
private PersistentLogger persistentLogger;
|
private PersistentLogger persistentLogger;
|
||||||
|
|
||||||
@Inject LokiAPIDatabase lokiAPIDatabase;
|
@Inject LokiAPIDatabase lokiAPIDatabase;
|
||||||
@Inject Storage storage;
|
@Inject public Storage storage;
|
||||||
|
@Inject Device device;
|
||||||
@Inject MessageDataProvider messageDataProvider;
|
@Inject MessageDataProvider messageDataProvider;
|
||||||
@Inject JobDatabase jobDatabase;
|
|
||||||
@Inject TextSecurePreferences textSecurePreferences;
|
@Inject TextSecurePreferences textSecurePreferences;
|
||||||
|
@Inject PushRegistry pushRegistry;
|
||||||
|
@Inject ConfigFactory configFactory;
|
||||||
CallMessageProcessor callMessageProcessor;
|
CallMessageProcessor callMessageProcessor;
|
||||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||||
|
|
||||||
|
@ -156,6 +166,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
return (ApplicationContext) context.getApplicationContext();
|
return (ApplicationContext) context.getApplicationContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TextSecurePreferences getPrefs() {
|
||||||
|
return EntryPoints.get(getApplicationContext(), AppComponent.class).getPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
public DatabaseComponent getDatabaseComponent() {
|
public DatabaseComponent getDatabaseComponent() {
|
||||||
return EntryPoints.get(getApplicationContext(), DatabaseComponent.class);
|
return EntryPoints.get(getApplicationContext(), DatabaseComponent.class);
|
||||||
}
|
}
|
||||||
|
@ -182,15 +196,30 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
return this.persistentLogger;
|
return this.persistentLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyUpdates(@NonNull ConfigBase forConfigObject) {
|
||||||
|
// forward to the config factory / storage ig
|
||||||
|
if (forConfigObject instanceof UserProfile && !textSecurePreferences.getConfigurationMessageSynced()) {
|
||||||
|
textSecurePreferences.setConfigurationMessageSynced(true);
|
||||||
|
}
|
||||||
|
storage.notifyConfigUpdates(forConfigObject);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
TextSecurePreferences.setPushSuffix(BuildConfig.PUSH_KEY_SUFFIX);
|
||||||
|
|
||||||
DatabaseModule.init(this);
|
DatabaseModule.init(this);
|
||||||
MessagingModuleConfiguration.configure(this);
|
MessagingModuleConfiguration.configure(this);
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
|
messagingModuleConfiguration = new MessagingModuleConfiguration(
|
||||||
|
this,
|
||||||
storage,
|
storage,
|
||||||
|
device,
|
||||||
messageDataProvider,
|
messageDataProvider,
|
||||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
|
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
||||||
|
configFactory
|
||||||
|
);
|
||||||
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
startKovenant();
|
startKovenant();
|
||||||
|
@ -204,11 +233,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
broadcaster = new Broadcaster(this);
|
broadcaster = new Broadcaster(this);
|
||||||
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
||||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
|
||||||
if (userPublicKey != null) {
|
|
||||||
registerForFCMIfNeeded(false);
|
|
||||||
}
|
|
||||||
UiModeUtilities.setupUiModeToUserSelected(this);
|
|
||||||
initializeExpiringMessageManager();
|
initializeExpiringMessageManager();
|
||||||
initializeTypingStatusRepository();
|
initializeTypingStatusRepository();
|
||||||
initializeTypingStatusSender();
|
initializeTypingStatusSender();
|
||||||
|
@ -216,10 +240,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
initializeProfileManager();
|
initializeProfileManager();
|
||||||
initializePeriodicTasks();
|
initializePeriodicTasks();
|
||||||
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
|
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
|
||||||
initializeJobManager();
|
|
||||||
initializeWebRtc();
|
initializeWebRtc();
|
||||||
initializeBlobProvider();
|
initializeBlobProvider();
|
||||||
resubmitProfilePictureIfNeeded();
|
resubmitProfilePictureIfNeeded();
|
||||||
|
loadEmojiSearchIndexIfNeeded();
|
||||||
|
EmojiSource.refresh();
|
||||||
|
|
||||||
|
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
|
||||||
|
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -228,6 +256,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
Log.i(TAG, "App is now visible.");
|
Log.i(TAG, "App is now visible.");
|
||||||
KeyCachingService.onAppForegrounded(this);
|
KeyCachingService.onAppForegrounded(this);
|
||||||
|
|
||||||
|
// If the user account hasn't been created or onboarding wasn't finished then don't start
|
||||||
|
// the pollers
|
||||||
|
if (TextSecurePreferences.getLocalNumber(this) == null || !TextSecurePreferences.hasSeenWelcomeScreen(this)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ThreadUtils.queue(()->{
|
ThreadUtils.queue(()->{
|
||||||
if (poller != null) {
|
if (poller != null) {
|
||||||
poller.setCaughtUp(false);
|
poller.setCaughtUp(false);
|
||||||
|
@ -248,7 +282,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
if (poller != null) {
|
if (poller != null) {
|
||||||
poller.stopIfNeeded();
|
poller.stopIfNeeded();
|
||||||
}
|
}
|
||||||
ClosedGroupPollerV2.getShared().stop();
|
ClosedGroupPollerV2.getShared().stopAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -262,10 +296,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
LocaleParser.Companion.configure(new LocaleParseHelper());
|
LocaleParser.Companion.configure(new LocaleParseHelper());
|
||||||
}
|
}
|
||||||
|
|
||||||
public JobManager getJobManager() {
|
|
||||||
return jobManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExpiringMessageManager getExpiringMessageManager() {
|
public ExpiringMessageManager getExpiringMessageManager() {
|
||||||
return expiringMessageManager;
|
return expiringMessageManager;
|
||||||
}
|
}
|
||||||
|
@ -328,16 +358,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
|
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeJobManager() {
|
|
||||||
this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
|
|
||||||
.setDataSerializer(new JsonDataSerializer())
|
|
||||||
.setJobFactories(JobManagerFactories.getJobFactories(this))
|
|
||||||
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
|
|
||||||
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
|
|
||||||
.setJobStorage(new FastJobStorage(jobDatabase))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeExpiringMessageManager() {
|
private void initializeExpiringMessageManager() {
|
||||||
this.expiringMessageManager = new ExpiringMessageManager(this);
|
this.expiringMessageManager = new ExpiringMessageManager(this);
|
||||||
}
|
}
|
||||||
|
@ -351,7 +371,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeProfileManager() {
|
private void initializeProfileManager() {
|
||||||
this.profileManager = new ProfileManager();
|
this.profileManager = new ProfileManager(this, configFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeTypingStatusSender() {
|
private void initializeTypingStatusSender() {
|
||||||
|
@ -360,10 +380,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
|
|
||||||
private void initializePeriodicTasks() {
|
private void initializePeriodicTasks() {
|
||||||
BackgroundPollWorker.schedulePeriodic(this);
|
BackgroundPollWorker.schedulePeriodic(this);
|
||||||
|
|
||||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
|
||||||
UpdateApkRefreshListener.schedule(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeWebRtc() {
|
private void initializeWebRtc() {
|
||||||
|
@ -414,29 +430,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ProviderInitializationException extends RuntimeException { }
|
private static class ProviderInitializationException extends RuntimeException { }
|
||||||
|
|
||||||
public void registerForFCMIfNeeded(final Boolean force) {
|
|
||||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
|
|
||||||
if (force && firebaseInstanceIdJob != null) {
|
|
||||||
firebaseInstanceIdJob.cancel(null);
|
|
||||||
}
|
|
||||||
firebaseInstanceIdJob = FcmUtils.getFcmInstanceId(task->{
|
|
||||||
if (!task.isSuccessful()) {
|
|
||||||
Log.w("Loki", "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.getException());
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
}
|
|
||||||
String token = task.getResult().getToken();
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
|
||||||
if (userPublicKey == null) return Unit.INSTANCE;
|
|
||||||
if (TextSecurePreferences.isUsingFCM(this)) {
|
|
||||||
LokiPushNotificationManager.register(token, userPublicKey, this, force);
|
|
||||||
} else {
|
|
||||||
LokiPushNotificationManager.unregister(token, this);
|
|
||||||
}
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setUpPollingIfNeeded() {
|
private void setUpPollingIfNeeded() {
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||||
if (userPublicKey == null) return;
|
if (userPublicKey == null) return;
|
||||||
|
@ -444,7 +437,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
poller.setUserPublicKey(userPublicKey);
|
poller.setUserPublicKey(userPublicKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
poller = new Poller();
|
poller = new Poller(configFactory, new Timer());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startPollingIfNeeded() {
|
public void startPollingIfNeeded() {
|
||||||
|
@ -465,6 +458,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
||||||
ThreadUtils.queue(() -> {
|
ThreadUtils.queue(() -> {
|
||||||
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
||||||
|
Log.d("Loki-Avatar", "Uploading Avatar Started");
|
||||||
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
|
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
|
||||||
try {
|
try {
|
||||||
// Read the file into a byte array
|
// Read the file into a byte array
|
||||||
|
@ -481,33 +475,46 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||||
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
|
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
|
||||||
// Update the last profile picture upload date
|
// Update the last profile picture upload date
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
|
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
|
||||||
|
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
Log.e("Loki-Avatar", "Uploading avatar failed", exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadEmojiSearchIndexIfNeeded() {
|
||||||
|
Executors.newSingleThreadExecutor().execute(() -> {
|
||||||
|
EmojiSearchDatabase emojiSearchDb = getDatabaseComponent().emojiSearchDatabase();
|
||||||
|
if (emojiSearchDb.query("face", 1).isEmpty()) {
|
||||||
|
try (InputStream inputStream = getAssets().open("emoji/emoji_search_index.json")) {
|
||||||
|
List<EmojiSearchData> searchIndex = Arrays.asList(JsonUtil.fromJson(inputStream, EmojiSearchData[].class));
|
||||||
|
emojiSearchDb.setSearchIndex(searchIndex);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("Loki", "Failed to load emoji search index");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||||
String token = TextSecurePreferences.getFCMToken(this);
|
|
||||||
if (token != null && !token.isEmpty()) {
|
|
||||||
LokiPushNotificationManager.unregister(token, this);
|
|
||||||
}
|
|
||||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
|
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
|
||||||
firebaseInstanceIdJob.cancel(null);
|
firebaseInstanceIdJob.cancel(null);
|
||||||
}
|
}
|
||||||
String displayName = TextSecurePreferences.getProfileName(this);
|
String displayName = TextSecurePreferences.getProfileName(this);
|
||||||
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
boolean isUsingFCM = TextSecurePreferences.isPushEnabled(this);
|
||||||
TextSecurePreferences.clearAll(this);
|
TextSecurePreferences.clearAll(this);
|
||||||
if (isMigratingToV2KeyPair) {
|
if (isMigratingToV2KeyPair) {
|
||||||
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
TextSecurePreferences.setPushEnabled(this, isUsingFCM);
|
||||||
TextSecurePreferences.setProfileName(this, displayName);
|
TextSecurePreferences.setProfileName(this, displayName);
|
||||||
}
|
}
|
||||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||||
if (!deleteDatabase("signal.db")) {
|
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
|
||||||
Log.d("Loki", "Failed to delete database.");
|
Log.d("Loki", "Failed to delete database.");
|
||||||
}
|
}
|
||||||
|
configFactory.keyPairChanged();
|
||||||
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
|
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,44 +1,110 @@
|
||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
|
import static org.session.libsession.utilities.TextSecurePreferences.SELECTED_ACCENT_COLOR;
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StyleRes;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
|
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.WindowUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.ActivityUtilitiesKt;
|
||||||
|
import org.thoughtcrime.securesms.util.ThemeState;
|
||||||
|
import org.thoughtcrime.securesms.util.UiModeUtilities;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public abstract class BaseActionBarActivity extends AppCompatActivity {
|
public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||||
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
|
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
|
||||||
|
public ThemeState currentThemeState;
|
||||||
|
|
||||||
|
private TextSecurePreferences getPreferences() {
|
||||||
|
ApplicationContext appContext = (ApplicationContext) getApplicationContext();
|
||||||
|
return appContext.textSecurePreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
@StyleRes
|
||||||
|
public int getDesiredTheme() {
|
||||||
|
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
|
||||||
|
int userSelectedTheme = themeState.getTheme();
|
||||||
|
if (themeState.getFollowSystem()) {
|
||||||
|
// do light or dark based on the selected theme
|
||||||
|
boolean isDayUi = UiModeUtilities.isDayUiMode(this);
|
||||||
|
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
|
||||||
|
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
|
||||||
|
} else {
|
||||||
|
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return userSelectedTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StyleRes @Nullable
|
||||||
|
public Integer getAccentTheme() {
|
||||||
|
if (!getPreferences().hasPreference(SELECTED_ACCENT_COLOR)) return null;
|
||||||
|
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
|
||||||
|
return themeState.getAccentStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Resources.Theme getTheme() {
|
||||||
|
// New themes
|
||||||
|
Resources.Theme modifiedTheme = super.getTheme();
|
||||||
|
modifiedTheme.applyStyle(getDesiredTheme(), true);
|
||||||
|
Integer accentTheme = getAccentTheme();
|
||||||
|
if (accentTheme != null) {
|
||||||
|
modifiedTheme.applyStyle(accentTheme, true);
|
||||||
|
}
|
||||||
|
currentThemeState = ActivityUtilitiesKt.themeState(getPreferences());
|
||||||
|
return modifiedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
ActionBar actionBar = getSupportActionBar();
|
ActionBar actionBar = getSupportActionBar();
|
||||||
if (actionBar != null) {
|
if (actionBar != null) {
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
actionBar.setHomeButtonEnabled(true);
|
actionBar.setHomeButtonEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
initializeScreenshotSecurity();
|
initializeScreenshotSecurity(true);
|
||||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||||
String name = getResources().getString(R.string.app_name);
|
String name = getResources().getString(R.string.app_name);
|
||||||
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground);
|
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground);
|
||||||
int color = getResources().getColor(R.color.app_icon_background);
|
int color = getResources().getColor(R.color.app_icon_background);
|
||||||
setTaskDescription(new ActivityManager.TaskDescription(name, icon, color));
|
setTaskDescription(new ActivityManager.TaskDescription(name, icon, color));
|
||||||
|
if (!currentThemeState.equals(ActivityUtilitiesKt.themeState(getPreferences()))) {
|
||||||
|
recreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply lightStatusBar manually as API 26 does not update properly via applyTheme
|
||||||
|
// https://issuetracker.google.com/issues/65883460?pli=1
|
||||||
|
if (SDK_INT >= 26 && SDK_INT <= 27) WindowUtil.setLightStatusBarFromTheme(this);
|
||||||
|
if (SDK_INT == 27) WindowUtil.setLightNavigationBarFromTheme(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
initializeScreenshotSecurity(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -49,11 +115,15 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeScreenshotSecurity() {
|
private void initializeScreenshotSecurity(boolean isResume) {
|
||||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
if (!isResume) {
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
} else {
|
} else {
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||||
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
|
} else {
|
||||||
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
|
||||||
import org.session.libsession.utilities.Address;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public interface BindableConversationItem extends Unbindable {
|
|
||||||
void bind(@NonNull MessageRecord messageRecord,
|
|
||||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
|
||||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
|
||||||
@NonNull GlideRequests glideRequests,
|
|
||||||
@NonNull Locale locale,
|
|
||||||
@NonNull Set<MessageRecord> batchSelected,
|
|
||||||
@NonNull Recipient recipients,
|
|
||||||
@Nullable String searchQuery,
|
|
||||||
boolean pulseHighlight);
|
|
||||||
|
|
||||||
MessageRecord getMessageRecord();
|
|
||||||
|
|
||||||
void setEventListener(@Nullable EventListener listener);
|
|
||||||
|
|
||||||
interface EventListener {
|
|
||||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
|
||||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
|
||||||
void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import network.loki.messenger.R
|
||||||
|
|
||||||
|
class DeleteMediaDialog {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
|
||||||
|
iconAttribute(R.attr.dialog_alert_icon)
|
||||||
|
title(
|
||||||
|
context.resources.getQuantityString(
|
||||||
|
R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
|
||||||
|
recordCount,
|
||||||
|
recordCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text(
|
||||||
|
context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
|
||||||
|
recordCount,
|
||||||
|
recordCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
button(R.string.delete) { doDelete.run() }
|
||||||
|
cancelButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import network.loki.messenger.R
|
||||||
|
|
||||||
|
class DeleteMediaPreviewDialog {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun show(context: Context, doDelete: Runnable) {
|
||||||
|
context.showSessionDialog {
|
||||||
|
iconAttribute(R.attr.dialog_alert_icon)
|
||||||
|
title(R.string.MediaPreviewActivity_media_delete_confirmation_title)
|
||||||
|
text(R.string.MediaPreviewActivity_media_delete_confirmation_message)
|
||||||
|
button(R.string.delete) { doDelete.run() }
|
||||||
|
cancelButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import network.loki.messenger.BuildConfig
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object DeviceModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provides() = BuildConfig.DEVICE
|
||||||
|
}
|
|
@ -1,88 +0,0 @@
|
||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.ExpirationUtil;
|
|
||||||
|
|
||||||
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class ExpirationDialog extends AlertDialog {
|
|
||||||
|
|
||||||
protected ExpirationDialog(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ExpirationDialog(Context context, int theme) {
|
|
||||||
super(context, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ExpirationDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
|
|
||||||
super(context, cancelable, cancelListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void show(final Context context,
|
|
||||||
final int currentExpiration,
|
|
||||||
final @NonNull OnClickListener listener)
|
|
||||||
{
|
|
||||||
final View view = createNumberPickerView(context, currentExpiration);
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
|
||||||
builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
|
|
||||||
builder.setView(view);
|
|
||||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
|
||||||
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
|
|
||||||
listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]);
|
|
||||||
});
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static View createNumberPickerView(final Context context, final int currentExpiration) {
|
|
||||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
|
||||||
final View view = inflater.inflate(R.layout.expiration_dialog, null);
|
|
||||||
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
|
|
||||||
final TextView textView = view.findViewById(R.id.expiration_details);
|
|
||||||
final int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
|
|
||||||
final String[] expirationDisplayValues = new String[expirationTimes.length];
|
|
||||||
|
|
||||||
int selectedIndex = expirationTimes.length - 1;
|
|
||||||
|
|
||||||
for (int i=0;i<expirationTimes.length;i++) {
|
|
||||||
expirationDisplayValues[i] = ExpirationUtil.getExpirationDisplayValue(context, expirationTimes[i]);
|
|
||||||
|
|
||||||
if ((currentExpiration >= expirationTimes[i]) &&
|
|
||||||
(i == expirationTimes.length -1 || currentExpiration < expirationTimes[i+1])) {
|
|
||||||
selectedIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
numberPickerView.setDisplayedValues(expirationDisplayValues);
|
|
||||||
numberPickerView.setMinValue(0);
|
|
||||||
numberPickerView.setMaxValue(expirationTimes.length-1);
|
|
||||||
|
|
||||||
NumberPickerView.OnValueChangeListener listener = (picker, oldVal, newVal) -> {
|
|
||||||
if (newVal == 0) {
|
|
||||||
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
|
|
||||||
} else {
|
|
||||||
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
numberPickerView.setOnValueChangedListener(listener);
|
|
||||||
numberPickerView.setValue(selectedIndex);
|
|
||||||
listener.onValueChange(numberPickerView, selectedIndex, selectedIndex);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnClickListener {
|
|
||||||
public void onClick(int expirationTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import cn.carbswang.android.numberpickerview.library.NumberPickerView
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.ExpirationUtil
|
||||||
|
|
||||||
|
fun Context.showExpirationDialog(
|
||||||
|
expiration: Int,
|
||||||
|
onExpirationTime: (Int) -> Unit
|
||||||
|
): AlertDialog {
|
||||||
|
val view = LayoutInflater.from(this).inflate(R.layout.expiration_dialog, null)
|
||||||
|
val numberPickerView = view.findViewById<NumberPickerView>(R.id.expiration_number_picker)
|
||||||
|
|
||||||
|
fun updateText(index: Int) {
|
||||||
|
view.findViewById<TextView>(R.id.expiration_details).text = when (index) {
|
||||||
|
0 -> getString(R.string.ExpirationDialog_your_messages_will_not_expire)
|
||||||
|
else -> getString(
|
||||||
|
R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen,
|
||||||
|
numberPickerView.displayedValues[index]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val expirationTimes = resources.getIntArray(R.array.expiration_times)
|
||||||
|
val expirationDisplayValues = expirationTimes
|
||||||
|
.map { ExpirationUtil.getExpirationDisplayValue(this, it) }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
val selectedIndex = expirationTimes.run { indexOfFirst { it >= expiration }.coerceIn(indices) }
|
||||||
|
|
||||||
|
numberPickerView.apply {
|
||||||
|
displayedValues = expirationDisplayValues
|
||||||
|
minValue = 0
|
||||||
|
maxValue = expirationTimes.lastIndex
|
||||||
|
setOnValueChangedListener { _, _, index -> updateText(index) }
|
||||||
|
value = selectedIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
updateText(selectedIndex)
|
||||||
|
|
||||||
|
return showSessionDialog {
|
||||||
|
title(getString(R.string.ExpirationDialog_disappearing_messages))
|
||||||
|
view(view)
|
||||||
|
okButton { onExpirationTime(numberPickerView.let { expirationTimes[it.value] }) }
|
||||||
|
cancelButton()
|
||||||
|
}
|
||||||
|
}
|
|
@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
|
||||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
||||||
|
|
||||||
if (slide != null) {
|
if (slide != null) {
|
||||||
thumbnailView.setImageResource(glideRequests, slide, false, false);
|
thumbnailView.setImageResource(glideRequests, slide, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||||
|
|
|
@ -54,6 +54,7 @@ import com.google.android.material.tabs.TabLayout;
|
||||||
|
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
|
import org.session.libsession.snode.SnodeAPI;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||||
|
@ -75,6 +76,7 @@ import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import kotlin.Unit;
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -317,9 +319,9 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||||
@SuppressWarnings("CodeBlock2Expr")
|
@SuppressWarnings("CodeBlock2Expr")
|
||||||
@SuppressLint({"InlinedApi", "StaticFieldLeak"})
|
@SuppressLint({"InlinedApi", "StaticFieldLeak"})
|
||||||
private void handleSaveMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
private void handleSaveMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
||||||
final Context context = getContext();
|
final Context context = requireContext();
|
||||||
|
|
||||||
SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> {
|
SaveAttachmentTask.showWarningDialog(context, mediaRecords.size(), () -> {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||||
|
@ -361,53 +363,39 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||||
}.execute();
|
}.execute();
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
}, mediaRecords.size());
|
return Unit.INSTANCE;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMediaSavedNotificationIfNeeded() {
|
private void sendMediaSavedNotificationIfNeeded() {
|
||||||
if (recipient.isGroupRecipient()) return;
|
if (recipient.isGroupRecipient()) return;
|
||||||
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
|
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
|
||||||
MessageSender.send(message, recipient.getAddress());
|
MessageSender.send(message, recipient.getAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
||||||
int recordCount = mediaRecords.size();
|
int recordCount = mediaRecords.size();
|
||||||
Resources res = getContext().getResources();
|
|
||||||
String confirmTitle = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
|
|
||||||
recordCount,
|
|
||||||
recordCount);
|
|
||||||
String confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
|
|
||||||
recordCount,
|
|
||||||
recordCount);
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
DeleteMediaDialog.show(
|
||||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
requireContext(),
|
||||||
builder.setTitle(confirmTitle);
|
recordCount,
|
||||||
builder.setMessage(confirmMessage);
|
() -> new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(
|
||||||
builder.setCancelable(true);
|
requireContext(),
|
||||||
|
R.string.MediaOverviewActivity_Media_delete_progress_title,
|
||||||
builder.setPositiveButton(R.string.delete, (dialogInterface, i) -> {
|
R.string.MediaOverviewActivity_Media_delete_progress_message) {
|
||||||
new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(getContext(),
|
@Override
|
||||||
R.string.MediaOverviewActivity_Media_delete_progress_title,
|
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
|
||||||
R.string.MediaOverviewActivity_Media_delete_progress_message)
|
if (records == null || records.length == 0) {
|
||||||
{
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
|
|
||||||
if (records == null || records.length == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (MediaDatabase.MediaRecord record : records) {
|
|
||||||
AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()]));
|
for (MediaDatabase.MediaRecord record : records) {
|
||||||
});
|
AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
}
|
||||||
builder.show();
|
return null;
|
||||||
|
}
|
||||||
|
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()])));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSelectAllMedia() {
|
private void handleSelectAllMedia() {
|
||||||
|
|
|
@ -47,7 +47,6 @@ import android.widget.Toast;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.core.util.Pair;
|
import androidx.core.util.Pair;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
|
@ -60,6 +59,7 @@ import androidx.viewpager.widget.ViewPager;
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||||
|
import org.session.libsession.snode.SnodeAPI;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
@ -84,6 +84,7 @@ import java.io.IOException;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
import kotlin.Unit;
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,6 +146,10 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) {
|
||||||
|
return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread());
|
||||||
|
}
|
||||||
|
|
||||||
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
|
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
|
||||||
Intent previewIntent = null;
|
Intent previewIntent = null;
|
||||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||||
|
@ -415,7 +420,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||||
MediaItem mediaItem = getCurrentMediaItem();
|
MediaItem mediaItem = getCurrentMediaItem();
|
||||||
if (mediaItem == null) return;
|
if (mediaItem == null) return;
|
||||||
|
|
||||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
SaveAttachmentTask.showWarningDialog(this, 1, () -> {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||||
|
@ -423,7 +428,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
|
||||||
saveTask.executeOnExecutor(
|
saveTask.executeOnExecutor(
|
||||||
AsyncTask.THREAD_POOL_EXECUTOR,
|
AsyncTask.THREAD_POOL_EXECUTOR,
|
||||||
new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
||||||
|
@ -432,12 +437,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMediaSavedNotificationIfNeeded() {
|
private void sendMediaSavedNotificationIfNeeded() {
|
||||||
if (conversationRecipient.isGroupRecipient()) return;
|
if (conversationRecipient.isGroupRecipient()) return;
|
||||||
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
|
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
|
||||||
MessageSender.send(message, conversationRecipient.getAddress());
|
MessageSender.send(message, conversationRecipient.getAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,29 +454,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
DeleteMediaPreviewDialog.show(this, () -> {
|
||||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
new AsyncTask<Void, Void, Void>() {
|
||||||
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
|
@Override
|
||||||
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
|
protected Void doInBackground(Void... voids) {
|
||||||
builder.setCancelable(true);
|
DatabaseAttachment attachment = mediaItem.attachment;
|
||||||
|
if (attachment != null) {
|
||||||
builder.setPositiveButton(R.string.delete, (dialogInterface, which) -> {
|
AttachmentUtil.deleteAttachment(getApplicationContext(), attachment);
|
||||||
new AsyncTask<Void, Void, Void>() {
|
}
|
||||||
@Override
|
return null;
|
||||||
protected Void doInBackground(Void... voids) {
|
}
|
||||||
if (mediaItem.attachment == null) {
|
}.execute();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
AttachmentUtil.deleteAttachment(MediaPreviewActivity.this.getApplicationContext(),
|
|
||||||
mediaItem.attachment);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.execute();
|
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
|
||||||
builder.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -530,18 +527,21 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
|
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
||||||
mediaPager.setAdapter(adapter);
|
mediaPager.setAdapter(adapter);
|
||||||
adapter.setActive(true);
|
adapter.setActive(true);
|
||||||
|
|
||||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||||
|
|
||||||
int item = restartItem >= 0 ? restartItem : data.second;
|
if (restartItem >= 0 || data.second >= 0) {
|
||||||
mediaPager.setCurrentItem(item);
|
int item = restartItem >= 0 ? restartItem : data.second;
|
||||||
|
mediaPager.setCurrentItem(item);
|
||||||
|
|
||||||
if (item == 0) {
|
if (item == 0) {
|
||||||
viewPagerListener.onPageSelected(0);
|
viewPagerListener.onPageSelected(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "one of restartItem "+restartItem+" and data.second "+data.second+" would cause OOB exception");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
|
||||||
|
data class MediaPreviewArgs(
|
||||||
|
val slide: Slide,
|
||||||
|
val mmsRecord: MmsMessageRecord?,
|
||||||
|
val thread: Recipient?,
|
||||||
|
)
|
|
@ -1,111 +0,0 @@
|
||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
import android.widget.BaseAdapter;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
||||||
import org.thoughtcrime.securesms.contacts.UserView;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsession.utilities.Conversions;
|
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final GlideRequests glideRequests;
|
|
||||||
private final MessageRecord record;
|
|
||||||
private final List<RecipientDeliveryStatus> members;
|
|
||||||
private final boolean isPushGroup;
|
|
||||||
|
|
||||||
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
|
||||||
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> members,
|
|
||||||
boolean isPushGroup)
|
|
||||||
{
|
|
||||||
this.context = context;
|
|
||||||
this.glideRequests = glideRequests;
|
|
||||||
this.record = record;
|
|
||||||
this.isPushGroup = isPushGroup;
|
|
||||||
this.members = members;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return members.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getItem(int position) {
|
|
||||||
return members.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
try {
|
|
||||||
return Conversions.byteArrayToLong(MessageDigest.getInstance("SHA1").digest(members.get(position).recipient.getAddress().serialize().getBytes()));
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
UserView result = new UserView(context);
|
|
||||||
Recipient recipient = members.get(position).getRecipient();
|
|
||||||
result.setOpenGroupThreadID(record.getThreadId());
|
|
||||||
result.bind(recipient, glideRequests, UserView.ActionIndicator.None, false);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMovedToScrapHeap(View view) {
|
|
||||||
((UserView)view).unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static class RecipientDeliveryStatus {
|
|
||||||
|
|
||||||
enum Status {
|
|
||||||
UNKNOWN, PENDING, SENT, DELIVERED, READ
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Recipient recipient;
|
|
||||||
private final Status deliveryStatus;
|
|
||||||
private final boolean isUnidentified;
|
|
||||||
private final long timestamp;
|
|
||||||
|
|
||||||
RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) {
|
|
||||||
this.recipient = recipient;
|
|
||||||
this.deliveryStatus = deliveryStatus;
|
|
||||||
this.isUnidentified = isUnidentified;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
Status getDeliveryStatus() {
|
|
||||||
return deliveryStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isUnidentified() {
|
|
||||||
return isUnidentified;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Recipient getRecipient() {
|
|
||||||
return recipient;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class MuteDialog extends AlertDialog {
|
|
||||||
|
|
||||||
|
|
||||||
protected MuteDialog(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MuteDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
|
|
||||||
super(context, cancelable, cancelListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MuteDialog(Context context, int theme) {
|
|
||||||
super(context, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
|
||||||
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
|
||||||
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, final int which) {
|
|
||||||
final long muteUntil;
|
|
||||||
|
|
||||||
switch (which) {
|
|
||||||
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2); break;
|
|
||||||
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
|
|
||||||
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
|
|
||||||
case 4: muteUntil = Long.MAX_VALUE; break;
|
|
||||||
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
|
||||||
}
|
|
||||||
|
|
||||||
listener.onMuted(muteUntil);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.show();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface MuteSelectionListener {
|
|
||||||
public void onMuted(long until);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
fun showMuteDialog(
|
||||||
|
context: Context,
|
||||||
|
onMuteDuration: (Long) -> Unit
|
||||||
|
): AlertDialog = context.showSessionDialog {
|
||||||
|
title(R.string.MuteDialog_mute_notifications)
|
||||||
|
items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
|
||||||
|
onMuteDuration(Option.values()[it].getTime())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
|
||||||
|
ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
|
||||||
|
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)),
|
||||||
|
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
|
||||||
|
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
|
||||||
|
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
|
||||||
|
|
||||||
|
constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration })
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.crypto.BiometricSecretProvider;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
||||||
|
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
@ -68,6 +69,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||||
|
|
||||||
private boolean authenticated;
|
private boolean authenticated;
|
||||||
private boolean failure;
|
private boolean failure;
|
||||||
|
private boolean hasSignatureObject = true;
|
||||||
|
|
||||||
private KeyCachingService keyCachingService;
|
private KeyCachingService keyCachingService;
|
||||||
|
|
||||||
|
@ -146,12 +148,11 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||||
// Finish and proceed with the next intent.
|
// Finish and proceed with the next intent.
|
||||||
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
||||||
if (nextIntent != null) {
|
if (nextIntent != null) {
|
||||||
startActivity(nextIntent);
|
try {
|
||||||
// try {
|
startActivity(nextIntent);
|
||||||
// startActivity(nextIntent);
|
} catch (java.lang.SecurityException e) {
|
||||||
// } catch (java.lang.SecurityException e) {
|
Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.", e);
|
||||||
// Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.");
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
@ -205,7 +206,22 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||||
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
|
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
|
||||||
Log.i(TAG, "Listening for fingerprints...");
|
Log.i(TAG, "Listening for fingerprints...");
|
||||||
fingerprintCancellationSignal = new CancellationSignal();
|
fingerprintCancellationSignal = new CancellationSignal();
|
||||||
fingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(biometricSecretProvider.getOrCreateBiometricSignature(this)), 0, fingerprintCancellationSignal, fingerprintListener, null);
|
Signature signature;
|
||||||
|
try {
|
||||||
|
signature = biometricSecretProvider.getOrCreateBiometricSignature(this);
|
||||||
|
hasSignatureObject = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
signature = null;
|
||||||
|
hasSignatureObject = false;
|
||||||
|
Log.e(TAG, "Error getting / creating signature", e);
|
||||||
|
}
|
||||||
|
fingerprintManager.authenticate(
|
||||||
|
signature == null ? null : new FingerprintManagerCompat.CryptoObject(signature),
|
||||||
|
0,
|
||||||
|
fingerprintCancellationSignal,
|
||||||
|
fingerprintListener,
|
||||||
|
null
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "firing intent...");
|
Log.i(TAG, "firing intent...");
|
||||||
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Session", "");
|
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Session", "");
|
||||||
|
@ -230,8 +246,22 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||||
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
|
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
|
||||||
Log.i(TAG, "onAuthenticationSucceeded");
|
Log.i(TAG, "onAuthenticationSucceeded");
|
||||||
if (result.getCryptoObject() == null || result.getCryptoObject().getSignature() == null) {
|
if (result.getCryptoObject() == null || result.getCryptoObject().getSignature() == null) {
|
||||||
// authentication failed
|
if (hasSignatureObject) {
|
||||||
onAuthenticationFailed();
|
// authentication failed
|
||||||
|
onAuthenticationFailed();
|
||||||
|
} else {
|
||||||
|
fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp);
|
||||||
|
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
|
||||||
|
fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
handleAuthenticated();
|
||||||
|
|
||||||
|
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||||
|
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Signature object now successfully unlocked
|
// Signature object now successfully unlocked
|
||||||
|
|
|
@ -9,13 +9,14 @@ import android.os.Bundle;
|
||||||
import androidx.annotation.IdRes;
|
import androidx.annotation.IdRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.onboarding.LandingActivity;
|
import org.thoughtcrime.securesms.onboarding.LandingActivity;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@ -168,7 +169,13 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||||
};
|
};
|
||||||
|
|
||||||
IntentFilter filter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
|
IntentFilter filter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
|
||||||
registerReceiver(clearKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
|
ContextCompat.registerReceiver(
|
||||||
|
this,
|
||||||
|
clearKeyReceiver, filter,
|
||||||
|
KeyCachingService.KEY_PERMISSION,
|
||||||
|
null,
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeClearKeyReceiver(Context context) {
|
private void removeClearKeyReceiver(Context context) {
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.LinearLayout.VERTICAL
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.setMargins
|
||||||
|
import androidx.core.view.setPadding
|
||||||
|
import androidx.core.view.updateMargins
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
|
|
||||||
|
|
||||||
|
@DslMarker
|
||||||
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
|
||||||
|
annotation class DialogDsl
|
||||||
|
|
||||||
|
@DialogDsl
|
||||||
|
class SessionDialogBuilder(val context: Context) {
|
||||||
|
|
||||||
|
private val dp20 = toPx(20, context.resources)
|
||||||
|
private val dp40 = toPx(40, context.resources)
|
||||||
|
|
||||||
|
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
|
||||||
|
|
||||||
|
private var dialog: AlertDialog? = null
|
||||||
|
private fun dismiss() = dialog?.dismiss()
|
||||||
|
|
||||||
|
private val topView = LinearLayout(context).apply { orientation = VERTICAL }
|
||||||
|
.also(dialogBuilder::setCustomTitle)
|
||||||
|
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
||||||
|
private val buttonLayout = LinearLayout(context)
|
||||||
|
|
||||||
|
private val root = LinearLayout(context).apply { orientation = VERTICAL }
|
||||||
|
.also(dialogBuilder::setView)
|
||||||
|
.apply {
|
||||||
|
addView(contentView)
|
||||||
|
addView(buttonLayout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun title(@StringRes id: Int) = title(context.getString(id))
|
||||||
|
|
||||||
|
fun title(text: CharSequence?) = title(text?.toString())
|
||||||
|
fun title(text: String?) {
|
||||||
|
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
|
||||||
|
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
|
||||||
|
text(text, style) {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||||
|
.apply { updateMargins(dp40, 0, dp40, dp20) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
|
||||||
|
text ?: return
|
||||||
|
TextView(context, null, 0, style)
|
||||||
|
.apply {
|
||||||
|
setText(text)
|
||||||
|
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||||
|
modify()
|
||||||
|
}.let(topView::addView)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun view(view: View) = contentView.addView(view)
|
||||||
|
|
||||||
|
fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
|
||||||
|
|
||||||
|
fun iconAttribute(@AttrRes icon: Int): AlertDialog.Builder = dialogBuilder.setIconAttribute(icon)
|
||||||
|
|
||||||
|
fun singleChoiceItems(
|
||||||
|
options: Collection<String>,
|
||||||
|
currentSelected: Int = 0,
|
||||||
|
onSelect: (Int) -> Unit
|
||||||
|
) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect)
|
||||||
|
|
||||||
|
fun singleChoiceItems(
|
||||||
|
options: Array<String>,
|
||||||
|
currentSelected: Int = 0,
|
||||||
|
onSelect: (Int) -> Unit
|
||||||
|
): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems(
|
||||||
|
options,
|
||||||
|
currentSelected
|
||||||
|
) { dialog, it -> onSelect(it); dialog.dismiss() }
|
||||||
|
|
||||||
|
fun items(
|
||||||
|
options: Array<String>,
|
||||||
|
onSelect: (Int) -> Unit
|
||||||
|
): AlertDialog.Builder = dialogBuilder.setItems(
|
||||||
|
options,
|
||||||
|
) { dialog, it -> onSelect(it); dialog.dismiss() }
|
||||||
|
|
||||||
|
fun destructiveButton(
|
||||||
|
@StringRes text: Int,
|
||||||
|
@StringRes contentDescription: Int,
|
||||||
|
listener: () -> Unit = {}
|
||||||
|
) = button(
|
||||||
|
text,
|
||||||
|
contentDescription,
|
||||||
|
R.style.Widget_Session_Button_Dialog_DestructiveText,
|
||||||
|
) { listener() }
|
||||||
|
|
||||||
|
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() }
|
||||||
|
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button) { listener() }
|
||||||
|
|
||||||
|
fun button(
|
||||||
|
@StringRes text: Int,
|
||||||
|
@StringRes contentDescriptionRes: Int = text,
|
||||||
|
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
|
||||||
|
dismiss: Boolean = true,
|
||||||
|
listener: (() -> Unit) = {}
|
||||||
|
) = Button(context, null, 0, style).apply {
|
||||||
|
setText(text)
|
||||||
|
contentDescription = resources.getString(contentDescriptionRes)
|
||||||
|
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f)
|
||||||
|
.apply { setMargins(toPx(20, resources)) }
|
||||||
|
setOnClickListener {
|
||||||
|
listener.invoke()
|
||||||
|
if (dismiss) dismiss()
|
||||||
|
}
|
||||||
|
}.let(buttonLayout::addView)
|
||||||
|
|
||||||
|
fun create(): AlertDialog = dialogBuilder.create().also { dialog = it }
|
||||||
|
fun show(): AlertDialog = dialogBuilder.show().also { dialog = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
|
SessionDialogBuilder(this).apply { build() }.show()
|
||||||
|
|
||||||
|
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
|
SessionDialogBuilder(requireContext()).apply { build() }.show()
|
||||||
|
fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
|
SessionDialogBuilder(requireContext()).apply { build() }.create()
|
|
@ -1,5 +0,0 @@
|
||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
public interface Unbindable {
|
|
||||||
public void unbind();
|
|
||||||
}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.thoughtcrime.securesms.animation;
|
||||||
|
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
|
||||||
|
public abstract class AnimationCompleteListener implements Animator.AnimatorListener {
|
||||||
|
@Override
|
||||||
|
public final void onAnimationStart(Animator animation) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract void onAnimationEnd(Animator animation);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onAnimationCancel(Animator animation) {}
|
||||||
|
@Override
|
||||||
|
public final void onAnimationRepeat(Animator animation) {}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package org.thoughtcrime.securesms.animation;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.Transformation;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class ResizeAnimation extends Animation {
|
||||||
|
|
||||||
|
private final View target;
|
||||||
|
private final int targetWidthPx;
|
||||||
|
private final int targetHeightPx;
|
||||||
|
|
||||||
|
private int startWidth;
|
||||||
|
private int startHeight;
|
||||||
|
|
||||||
|
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
|
||||||
|
this.target = target;
|
||||||
|
this.targetWidthPx = targetWidthPx;
|
||||||
|
this.targetHeightPx = targetHeightPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||||
|
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
|
||||||
|
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams params = target.getLayoutParams();
|
||||||
|
|
||||||
|
params.width = newWidth;
|
||||||
|
params.height = newHeight;
|
||||||
|
|
||||||
|
target.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(int width, int height, int parentWidth, int parentHeight) {
|
||||||
|
super.initialize(width, height, parentWidth, parentHeight);
|
||||||
|
|
||||||
|
this.startWidth = width;
|
||||||
|
this.startHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean willChangeBounds() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,10 +74,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||||
attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value)
|
attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? {
|
override fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>? {
|
||||||
val messagingDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
|
val messagingDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val message = messagingDatabase.getMessageFor(timestamp, author)
|
val message = messagingDatabase.getMessageFor(timestamp, author)
|
||||||
return if (message != null) Pair(message.id, message.isMms) else null
|
return if (message != null) Triple(message.id, message.isMms, message.body) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment> {
|
override fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment> {
|
||||||
|
@ -176,6 +176,11 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||||
return messageDB.getMessageID(serverId, threadId)
|
return messageDB.getMessageID(serverId, threadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMessageIDs(serverIds: List<Long>, threadId: Long): Pair<List<Long>, List<Long>> {
|
||||||
|
val messageDB = DatabaseComponent.get(context).lokiMessageDatabase()
|
||||||
|
return messageDB.getMessageIDs(serverIds, threadId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
else DatabaseComponent.get(context).mmsDatabase()
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
@ -184,16 +189,27 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessageAsDeleted(timestamp: Long, author: String) {
|
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
||||||
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
|
||||||
|
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
||||||
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
||||||
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val address = Address.fromSerialized(author)
|
val address = Address.fromSerialized(author)
|
||||||
val message = database.getMessageFor(timestamp, address) ?: return
|
val message = database.getMessageFor(timestamp, address) ?: return null
|
||||||
val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase()
|
val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase()
|
||||||
else DatabaseComponent.get(context).smsDatabase()
|
else DatabaseComponent.get(context).smsDatabase()
|
||||||
messagingDatabase.markAsDeleted(message.id, message.isRead)
|
messagingDatabase.markAsDeleted(message.id, message.isRead, message.hasMention)
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
messagingDatabase.deleteMessage(message.id)
|
messagingDatabase.deleteMessage(message.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return message.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getServerHashForMessage(messageID: Long): String? {
|
override fun getServerHashForMessage(messageID: Long): String? {
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package org.thoughtcrime.securesms.attachments
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.ContentObserver
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer
|
||||||
|
|
||||||
|
private const val TAG = "ScreenshotObserver"
|
||||||
|
|
||||||
|
class ScreenshotObserver(private val context: Context, handler: Handler, private val screenshotTriggered: ()->Unit): ContentObserver(handler) {
|
||||||
|
|
||||||
|
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||||
|
super.onChange(selfChange, uri)
|
||||||
|
uri ?: return
|
||||||
|
|
||||||
|
// There is an odd bug where we can get notified for changes to 'content://media/external'
|
||||||
|
// directly which is a protected folder, this code is to prevent that crash
|
||||||
|
if (uri.scheme == "content" && uri.host == "media" && uri.path == "/external") { return }
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
queryRelativeDataColumn(uri)
|
||||||
|
} else {
|
||||||
|
queryDataColumn(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cache = mutableSetOf<Int>()
|
||||||
|
|
||||||
|
private fun queryDataColumn(uri: Uri) {
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Images.Media.DATA
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
context.contentResolver.query(
|
||||||
|
uri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val path = cursor.getString(dataColumn)
|
||||||
|
if (path.contains("screenshot", true)) {
|
||||||
|
if (cache.add(uri.hashCode())) {
|
||||||
|
screenshotTriggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.e(TAG, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun queryRelativeDataColumn(uri: Uri) {
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Images.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Images.Media.RELATIVE_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.contentResolver.query(
|
||||||
|
uri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val relativePathColumn =
|
||||||
|
cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH)
|
||||||
|
val displayNameColumn =
|
||||||
|
cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(displayNameColumn)
|
||||||
|
val relativePath = cursor.getString(relativePathColumn)
|
||||||
|
if (name.contains("screenshot", true) or
|
||||||
|
relativePath.contains("screenshot", true)) {
|
||||||
|
if (cache.add(uri.hashCode())) {
|
||||||
|
screenshotTriggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
Log.e(TAG, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,104 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.backup;
|
|
||||||
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.thoughtcrime.securesms.util.BackupDirSelector;
|
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class BackupDialog {
|
|
||||||
private static final String TAG = "BackupDialog";
|
|
||||||
|
|
||||||
public static void showEnableBackupDialog(
|
|
||||||
@NonNull Context context,
|
|
||||||
@NonNull SwitchPreferenceCompat preference,
|
|
||||||
@NonNull BackupDirSelector backupDirSelector) {
|
|
||||||
|
|
||||||
String[] password = BackupUtil.generateBackupPassphrase();
|
|
||||||
String passwordSt = Util.join(password, "");
|
|
||||||
|
|
||||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
|
||||||
.setView(R.layout.backup_enable_dialog)
|
|
||||||
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.create();
|
|
||||||
|
|
||||||
dialog.setOnShowListener(created -> {
|
|
||||||
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
|
|
||||||
button.setOnClickListener(v -> {
|
|
||||||
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
|
||||||
if (confirmationCheckBox.isChecked()) {
|
|
||||||
backupDirSelector.selectBackupDir(true, uri -> {
|
|
||||||
try {
|
|
||||||
BackupUtil.enableBackups(context, passwordSt);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to activate backups.", e);
|
|
||||||
Toast.makeText(context,
|
|
||||||
context.getString(R.string.dialog_backup_activation_failed),
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setChecked(true);
|
|
||||||
created.dismiss();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
CheckBox checkBox = dialog.findViewById(R.id.confirmation_check);
|
|
||||||
TextView textView = dialog.findViewById(R.id.confirmation_text);
|
|
||||||
|
|
||||||
((TextView)dialog.findViewById(R.id.code_first)).setText(password[0]);
|
|
||||||
((TextView)dialog.findViewById(R.id.code_second)).setText(password[1]);
|
|
||||||
((TextView)dialog.findViewById(R.id.code_third)).setText(password[2]);
|
|
||||||
|
|
||||||
((TextView)dialog.findViewById(R.id.code_fourth)).setText(password[3]);
|
|
||||||
((TextView)dialog.findViewById(R.id.code_fifth)).setText(password[4]);
|
|
||||||
((TextView)dialog.findViewById(R.id.code_sixth)).setText(password[5]);
|
|
||||||
|
|
||||||
textView.setOnClickListener(v -> checkBox.toggle());
|
|
||||||
|
|
||||||
dialog.findViewById(R.id.number_table).setOnClickListener(v -> {
|
|
||||||
((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", passwordSt));
|
|
||||||
Toast.makeText(context, R.string.BackupDialog_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.BackupDialog_delete_backups)
|
|
||||||
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
|
|
||||||
BackupUtil.disableBackups(context, true);
|
|
||||||
preference.setChecked(false);
|
|
||||||
})
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.backup
|
|
||||||
|
|
||||||
data class BackupEvent constructor(val type: Type, val count: Int, val exception: Exception?) {
|
|
||||||
|
|
||||||
enum class Type {
|
|
||||||
PROGRESS, FINISHED
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic fun createProgress(count: Int) = BackupEvent(Type.PROGRESS, count, null)
|
|
||||||
@JvmStatic fun createFinished() = BackupEvent(Type.FINISHED, 0, null)
|
|
||||||
@JvmStatic fun createFinished(e: Exception?) = BackupEvent(Type.FINISHED, 0, e)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.backup;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows the getting and setting of the backup passphrase, which is stored encrypted on API >= 23.
|
|
||||||
*/
|
|
||||||
public class BackupPassphrase {
|
|
||||||
|
|
||||||
private static final String TAG = BackupPassphrase.class.getSimpleName();
|
|
||||||
|
|
||||||
public static @Nullable String get(@NonNull Context context) {
|
|
||||||
String passphrase = TextSecurePreferences.getBackupPassphrase(context);
|
|
||||||
String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < 23 || (passphrase == null && encryptedPassphrase == null)) {
|
|
||||||
return passphrase;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encryptedPassphrase == null) {
|
|
||||||
Log.i(TAG, "Migrating to encrypted passphrase.");
|
|
||||||
set(context, passphrase);
|
|
||||||
encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyStoreHelper.SealedData data = KeyStoreHelper.SealedData.fromString(encryptedPassphrase);
|
|
||||||
return new String(KeyStoreHelper.unseal(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void set(@NonNull Context context, @Nullable String passphrase) {
|
|
||||||
if (passphrase == null || Build.VERSION.SDK_INT < 23) {
|
|
||||||
TextSecurePreferences.setBackupPassphrase(context, passphrase);
|
|
||||||
TextSecurePreferences.setEncryptedBackupPassphrase(context, null);
|
|
||||||
} else {
|
|
||||||
KeyStoreHelper.SealedData encryptedPassphrase = KeyStoreHelper.seal(passphrase.getBytes());
|
|
||||||
TextSecurePreferences.setEncryptedBackupPassphrase(context, encryptedPassphrase.serialize());
|
|
||||||
TextSecurePreferences.setBackupPassphrase(context, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.backup
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Build
|
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.preference.PreferenceManager.getDefaultSharedPreferencesName
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_BOOLEAN
|
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_INT
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
object BackupPreferences {
|
|
||||||
// region Backup related
|
|
||||||
fun getBackupRecords(context: Context): List<BackupProtos.SharedPreference> {
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val prefsFileName: String
|
|
||||||
prefsFileName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
getDefaultSharedPreferencesName(context)
|
|
||||||
} else {
|
|
||||||
context.packageName + "_preferences"
|
|
||||||
}
|
|
||||||
val prefList: LinkedList<BackupProtos.SharedPreference> = LinkedList<BackupProtos.SharedPreference>()
|
|
||||||
addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_REGISTRATION_ID_PREF)
|
|
||||||
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_NUMBER_PREF)
|
|
||||||
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_NAME_PREF)
|
|
||||||
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_URL_PREF)
|
|
||||||
addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_ID_PREF)
|
|
||||||
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_KEY_PREF)
|
|
||||||
addBackupEntryBoolean(prefList, preferences, prefsFileName, TextSecurePreferences.IS_USING_FCM)
|
|
||||||
return prefList
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addBackupEntryString(
|
|
||||||
outPrefList: MutableList<BackupProtos.SharedPreference>,
|
|
||||||
prefs: SharedPreferences,
|
|
||||||
prefFileName: String,
|
|
||||||
prefKey: String,
|
|
||||||
) {
|
|
||||||
val value = prefs.getString(prefKey, null)
|
|
||||||
if (value == null) {
|
|
||||||
logBackupEntry(prefKey, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
|
|
||||||
.setFile(prefFileName)
|
|
||||||
.setKey(prefKey)
|
|
||||||
.setValue(value)
|
|
||||||
.build())
|
|
||||||
logBackupEntry(prefKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addBackupEntryInt(
|
|
||||||
outPrefList: MutableList<BackupProtos.SharedPreference>,
|
|
||||||
prefs: SharedPreferences,
|
|
||||||
prefFileName: String,
|
|
||||||
prefKey: String,
|
|
||||||
) {
|
|
||||||
val value = prefs.getInt(prefKey, -1)
|
|
||||||
if (value == -1) {
|
|
||||||
logBackupEntry(prefKey, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
|
|
||||||
.setFile(prefFileName)
|
|
||||||
.setKey(PREF_PREFIX_TYPE_INT + prefKey) // The prefix denotes the type of the preference.
|
|
||||||
.setValue(value.toString())
|
|
||||||
.build())
|
|
||||||
logBackupEntry(prefKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addBackupEntryBoolean(
|
|
||||||
outPrefList: MutableList<BackupProtos.SharedPreference>,
|
|
||||||
prefs: SharedPreferences,
|
|
||||||
prefFileName: String,
|
|
||||||
prefKey: String,
|
|
||||||
) {
|
|
||||||
if (!prefs.contains(prefKey)) {
|
|
||||||
logBackupEntry(prefKey, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
|
|
||||||
.setFile(prefFileName)
|
|
||||||
.setKey(PREF_PREFIX_TYPE_BOOLEAN + prefKey) // The prefix denotes the type of the preference.
|
|
||||||
.setValue(prefs.getBoolean(prefKey, false).toString())
|
|
||||||
.build())
|
|
||||||
logBackupEntry(prefKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logBackupEntry(prefName: String, wasIncluded: Boolean) {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
sb.append("Backup preference ")
|
|
||||||
sb.append(if (wasIncluded) "+ " else "- ")
|
|
||||||
sb.append('\"').append(prefName).append("\" ")
|
|
||||||
if (!wasIncluded) {
|
|
||||||
sb.append("(is empty and not included)")
|
|
||||||
}
|
|
||||||
Log.d("Loki", sb.toString())
|
|
||||||
} // endregion
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.backup
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.style.ClickableSpan
|
|
||||||
import android.text.style.StyleSpan
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.google.android.gms.common.util.Strings
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
|
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|
||||||
import org.thoughtcrime.securesms.util.show
|
|
||||||
|
|
||||||
class BackupRestoreActivity : BaseActionBarActivity() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "BackupRestoreActivity"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val viewModel by viewModels<BackupRestoreViewModel>()
|
|
||||||
|
|
||||||
private val fileSelectionResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()
|
|
||||||
) { result: ActivityResult ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK && result.data != null && result.data!!.data != null) {
|
|
||||||
viewModel.backupFile.value = result.data!!.data!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setUpActionBarSessionLogo()
|
|
||||||
|
|
||||||
// val viewBinding = DataBindingUtil.setContentView<ActivityBackupRestoreBinding>(this, R.layout.activity_backup_restore)
|
|
||||||
// viewBinding.lifecycleOwner = this
|
|
||||||
// viewBinding.viewModel = viewModel
|
|
||||||
|
|
||||||
// viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() }
|
|
||||||
|
|
||||||
// viewBinding.buttonSelectFile.setOnClickListener {
|
|
||||||
// fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
// //FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly
|
|
||||||
// // and the backup files are unavailable for selection.
|
|
||||||
//// type = BackupUtil.BACKUP_FILE_MIME_TYPE
|
|
||||||
// type = "*/*"
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// viewBinding.backupCode.addTextChangedListener { text -> viewModel.backupPassphrase.value = text.toString() }
|
|
||||||
|
|
||||||
// Focus passphrase text edit when backup file is selected.
|
|
||||||
// viewModel.backupFile.observe(this, { backupFile ->
|
|
||||||
// if (backupFile != null) viewBinding.backupCode.post {
|
|
||||||
// viewBinding.backupCode.requestFocus()
|
|
||||||
// (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
|
|
||||||
// .showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// React to backup import result.
|
|
||||||
viewModel.backupImportResult.observe(this) { result ->
|
|
||||||
if (result != null) when (result) {
|
|
||||||
BackupRestoreViewModel.BackupRestoreResult.SUCCESS -> {
|
|
||||||
val intent = Intent(this, HomeActivity::class.java)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
this.show(intent)
|
|
||||||
}
|
|
||||||
BackupRestoreViewModel.BackupRestoreResult.FAILURE_VERSION_DOWNGRADE ->
|
|
||||||
Toast.makeText(this, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
|
|
||||||
BackupRestoreViewModel.BackupRestoreResult.FAILURE_UNKNOWN ->
|
|
||||||
Toast.makeText(this, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//region Legal info views
|
|
||||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
|
||||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
openURL("https://getsession.org/terms-of-service/")
|
|
||||||
}
|
|
||||||
}, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
openURL("https://getsession.org/privacy-policy/")
|
|
||||||
}
|
|
||||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
// viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
// viewBinding.termsTextView.text = termsExplanation
|
|
||||||
//endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openURL(url: String) {
|
|
||||||
try {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BackupRestoreViewModel(application: Application): AndroidViewModel(application) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "BackupRestoreViewModel"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun uriToFileName(view: View, fileUri: Uri?): String? {
|
|
||||||
fileUri ?: return null
|
|
||||||
|
|
||||||
view.context.contentResolver.query(fileUri, null, null, null, null).use {
|
|
||||||
val nameIndex = it!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
||||||
it.moveToFirst()
|
|
||||||
return it.getString(nameIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun validateData(fileUri: Uri?, passphrase: String?): Boolean {
|
|
||||||
return fileUri != null &&
|
|
||||||
!Strings.isEmptyOrWhitespace(passphrase) &&
|
|
||||||
passphrase!!.length == BackupUtil.BACKUP_PASSPHRASE_LENGTH
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val backupFile = MutableLiveData<Uri>(null)
|
|
||||||
val backupPassphrase = MutableLiveData<String>(null)
|
|
||||||
|
|
||||||
val processingBackupFile = MutableLiveData<Boolean>(false)
|
|
||||||
val backupImportResult = MutableLiveData<BackupRestoreResult>(null)
|
|
||||||
|
|
||||||
fun tryRestoreBackup() = viewModelScope.launch {
|
|
||||||
if (processingBackupFile.value == true) return@launch
|
|
||||||
if (backupImportResult.value == BackupRestoreResult.SUCCESS) return@launch
|
|
||||||
if (!validateData(backupFile.value, backupPassphrase.value)) return@launch
|
|
||||||
|
|
||||||
val context = getApplication<Application>()
|
|
||||||
val backupFile = backupFile.value!!
|
|
||||||
val passphrase = backupPassphrase.value!!
|
|
||||||
|
|
||||||
val result: BackupRestoreResult
|
|
||||||
|
|
||||||
processingBackupFile.value = true
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
result = try {
|
|
||||||
val database = DatabaseComponent.get(context).openHelper().readableDatabase
|
|
||||||
FullBackupImporter.importFromUri(
|
|
||||||
context,
|
|
||||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
|
||||||
database,
|
|
||||||
backupFile,
|
|
||||||
passphrase
|
|
||||||
)
|
|
||||||
DatabaseFactory.upgradeRestored(context, database)
|
|
||||||
NotificationChannels.restoreContactNotificationChannels(context)
|
|
||||||
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
|
|
||||||
TextSecurePreferences.setHasViewedSeed(context, true)
|
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
|
||||||
|
|
||||||
BackupRestoreResult.SUCCESS
|
|
||||||
} catch (e: DatabaseDowngradeException) {
|
|
||||||
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
|
|
||||||
BackupRestoreResult.FAILURE_VERSION_DOWNGRADE
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, e)
|
|
||||||
BackupRestoreResult.FAILURE_UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processingBackupFile.value = false
|
|
||||||
|
|
||||||
backupImportResult.value = result
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class BackupRestoreResult {
|
|
||||||
SUCCESS, FAILURE_VERSION_DOWNGRADE, FAILURE_UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,447 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.backup
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
|
||||||
import android.text.TextUtils
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import com.annimon.stream.function.Consumer
|
|
||||||
import com.annimon.stream.function.Predicate
|
|
||||||
import com.google.protobuf.ByteString
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
import org.session.libsession.avatars.AvatarHelper
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
|
||||||
import org.session.libsession.utilities.Conversions
|
|
||||||
import org.session.libsession.utilities.Util
|
|
||||||
import org.session.libsignal.crypto.kdf.HKDFv3
|
|
||||||
import org.session.libsignal.utilities.ByteUtil
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.Header
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.Sticker
|
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
|
||||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
|
||||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.JobDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
|
||||||
import org.thoughtcrime.securesms.database.PushDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.Flushable
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.LinkedList
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.Mac
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
object FullBackupExporter {
|
|
||||||
private val TAG = FullBackupExporter::class.java.simpleName
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@WorkerThread
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun export(context: Context,
|
|
||||||
attachmentSecret: AttachmentSecret,
|
|
||||||
input: SQLiteDatabase,
|
|
||||||
fileUri: Uri,
|
|
||||||
passphrase: String) {
|
|
||||||
|
|
||||||
val baseOutputStream = context.contentResolver.openOutputStream(fileUri)
|
|
||||||
?: throw IOException("Cannot open an output stream for the file URI: $fileUri")
|
|
||||||
|
|
||||||
var count = 0
|
|
||||||
try {
|
|
||||||
BackupFrameOutputStream(baseOutputStream, passphrase).use { outputStream ->
|
|
||||||
outputStream.writeDatabaseVersion(input.version)
|
|
||||||
val tables = exportSchema(input, outputStream)
|
|
||||||
for (table in tables) if (shouldExportTable(table)) {
|
|
||||||
count = when (table) {
|
|
||||||
SmsDatabase.TABLE_NAME, MmsDatabase.TABLE_NAME -> {
|
|
||||||
exportTable(table, input, outputStream,
|
|
||||||
{ cursor: Cursor ->
|
|
||||||
cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
count)
|
|
||||||
}
|
|
||||||
GroupReceiptDatabase.TABLE_NAME -> {
|
|
||||||
exportTable(table, input, outputStream,
|
|
||||||
{ cursor: Cursor ->
|
|
||||||
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID)))
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
count)
|
|
||||||
}
|
|
||||||
AttachmentDatabase.TABLE_NAME -> {
|
|
||||||
exportTable(table, input, outputStream,
|
|
||||||
{ cursor: Cursor ->
|
|
||||||
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)))
|
|
||||||
},
|
|
||||||
{ cursor: Cursor ->
|
|
||||||
exportAttachment(attachmentSecret, cursor, outputStream)
|
|
||||||
},
|
|
||||||
count)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
exportTable(table, input, outputStream, null, null, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (preference in BackupUtil.getBackupRecords(context)) {
|
|
||||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
|
||||||
outputStream.writePreferenceEntry(preference)
|
|
||||||
}
|
|
||||||
for (preference in BackupPreferences.getBackupRecords(context)) {
|
|
||||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
|
||||||
outputStream.writePreferenceEntry(preference)
|
|
||||||
}
|
|
||||||
for (avatar in AvatarHelper.getAvatarFiles(context)) {
|
|
||||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
|
||||||
outputStream.writeAvatar(avatar.name, FileInputStream(avatar), avatar.length())
|
|
||||||
}
|
|
||||||
outputStream.writeEnd()
|
|
||||||
}
|
|
||||||
EventBus.getDefault().post(BackupEvent.createFinished())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to make full backup.", e)
|
|
||||||
EventBus.getDefault().post(BackupEvent.createFinished(e))
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun shouldExportTable(table: String): Boolean {
|
|
||||||
return table != PushDatabase.TABLE_NAME &&
|
|
||||||
|
|
||||||
table != LokiBackupFilesDatabase.TABLE_NAME &&
|
|
||||||
table != LokiAPIDatabase.openGroupProfilePictureTable &&
|
|
||||||
|
|
||||||
table != JobDatabase.Jobs.TABLE_NAME &&
|
|
||||||
table != JobDatabase.Constraints.TABLE_NAME &&
|
|
||||||
table != JobDatabase.Dependencies.TABLE_NAME &&
|
|
||||||
|
|
||||||
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
|
|
||||||
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
|
|
||||||
!table.startsWith("sqlite_")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun exportSchema(input: SQLiteDatabase, outputStream: BackupFrameOutputStream): List<String> {
|
|
||||||
val tables: MutableList<String> = LinkedList()
|
|
||||||
input.rawQuery("SELECT sql, name, type FROM sqlite_master", null).use { cursor ->
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
val sql = cursor.getString(0)
|
|
||||||
val name = cursor.getString(1)
|
|
||||||
val type = cursor.getString(2)
|
|
||||||
if (sql != null) {
|
|
||||||
val isSmsFtsSecretTable = name != null && name != SearchDatabase.SMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME)
|
|
||||||
val isMmsFtsSecretTable = name != null && name != SearchDatabase.MMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME)
|
|
||||||
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
|
|
||||||
if ("table" == type) {
|
|
||||||
tables.add(name)
|
|
||||||
}
|
|
||||||
outputStream.writeSql(SqlStatement.newBuilder().setStatement(cursor.getString(0)).build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tables
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun exportTable(table: String,
|
|
||||||
input: SQLiteDatabase,
|
|
||||||
outputStream: BackupFrameOutputStream,
|
|
||||||
predicate: Predicate<Cursor>?,
|
|
||||||
postProcess: Consumer<Cursor>?,
|
|
||||||
count: Int): Int {
|
|
||||||
var count = count
|
|
||||||
val template = "INSERT INTO $table VALUES "
|
|
||||||
input.rawQuery("SELECT * FROM $table", null).use { cursor ->
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
|
||||||
if (predicate != null && !predicate.test(cursor)) continue
|
|
||||||
|
|
||||||
val statement = StringBuilder(template)
|
|
||||||
val statementBuilder = SqlStatement.newBuilder()
|
|
||||||
statement.append('(')
|
|
||||||
for (i in 0 until cursor.columnCount) {
|
|
||||||
statement.append('?')
|
|
||||||
when (cursor.getType(i)) {
|
|
||||||
Cursor.FIELD_TYPE_STRING -> {
|
|
||||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
|
||||||
.setStringParamter(cursor.getString(i)))
|
|
||||||
}
|
|
||||||
Cursor.FIELD_TYPE_FLOAT -> {
|
|
||||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
|
||||||
.setDoubleParameter(cursor.getDouble(i)))
|
|
||||||
}
|
|
||||||
Cursor.FIELD_TYPE_INTEGER -> {
|
|
||||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
|
||||||
.setIntegerParameter(cursor.getLong(i)))
|
|
||||||
}
|
|
||||||
Cursor.FIELD_TYPE_BLOB -> {
|
|
||||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
|
||||||
.setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))))
|
|
||||||
}
|
|
||||||
Cursor.FIELD_TYPE_NULL -> {
|
|
||||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
|
||||||
.setNullparameter(true))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw AssertionError("unknown type?" + cursor.getType(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i < cursor.columnCount - 1) {
|
|
||||||
statement.append(',')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statement.append(')')
|
|
||||||
outputStream.writeSql(statementBuilder.setStatement(statement.toString()).build())
|
|
||||||
postProcess?.accept(cursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exportAttachment(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
|
|
||||||
try {
|
|
||||||
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID))
|
|
||||||
val uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))
|
|
||||||
var size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE))
|
|
||||||
val data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA))
|
|
||||||
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM))
|
|
||||||
if (!TextUtils.isEmpty(data) && size <= 0) {
|
|
||||||
size = calculateVeryOldStreamLength(attachmentSecret, random, data)
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(data) && size > 0) {
|
|
||||||
val inputStream: InputStream = if (random != null && random.size == 32) {
|
|
||||||
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
|
|
||||||
} else {
|
|
||||||
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
|
|
||||||
}
|
|
||||||
outputStream.writeAttachment(AttachmentId(rowId, uniqueId), inputStream, size)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.w(TAG, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun calculateVeryOldStreamLength(attachmentSecret: AttachmentSecret, random: ByteArray?, data: String): Long {
|
|
||||||
var result: Long = 0
|
|
||||||
val inputStream: InputStream = if (random != null && random.size == 32) {
|
|
||||||
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
|
|
||||||
} else {
|
|
||||||
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
|
|
||||||
}
|
|
||||||
var read: Int
|
|
||||||
val buffer = ByteArray(8192)
|
|
||||||
while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) {
|
|
||||||
result += read.toLong()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
|
|
||||||
val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
|
|
||||||
val where = MmsSmsColumns.ID + " = ?"
|
|
||||||
val args = arrayOf(mmsId.toString())
|
|
||||||
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
|
|
||||||
if (mmsCursor != null && mmsCursor.moveToFirst()) {
|
|
||||||
return mmsCursor.getLong(0) == 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BackupFrameOutputStream : Closeable, Flushable {
|
|
||||||
|
|
||||||
private val outputStream: OutputStream
|
|
||||||
private var cipher: Cipher
|
|
||||||
private var mac: Mac
|
|
||||||
private val cipherKey: ByteArray
|
|
||||||
private val macKey: ByteArray
|
|
||||||
private val iv: ByteArray
|
|
||||||
|
|
||||||
private var counter: Int = 0
|
|
||||||
|
|
||||||
constructor(outputStream: OutputStream, passphrase: String) : super() {
|
|
||||||
try {
|
|
||||||
val salt = Util.getSecretBytes(32)
|
|
||||||
val key = BackupUtil.computeBackupKey(passphrase, salt)
|
|
||||||
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
|
|
||||||
val split = ByteUtil.split(derived, 32, 32)
|
|
||||||
cipherKey = split[0]
|
|
||||||
macKey = split[1]
|
|
||||||
cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
|
||||||
mac = Mac.getInstance("HmacSHA256")
|
|
||||||
this.outputStream = outputStream
|
|
||||||
iv = Util.getSecretBytes(16)
|
|
||||||
counter = Conversions.byteArrayToInt(iv)
|
|
||||||
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
|
|
||||||
val header = BackupFrame.newBuilder().setHeader(Header.newBuilder()
|
|
||||||
.setIv(ByteString.copyFrom(iv))
|
|
||||||
.setSalt(ByteString.copyFrom(salt)))
|
|
||||||
.build().toByteArray()
|
|
||||||
outputStream.write(Conversions.intToByteArray(header.size))
|
|
||||||
outputStream.write(header)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is NoSuchAlgorithmException,
|
|
||||||
is NoSuchPaddingException,
|
|
||||||
is InvalidKeyException -> {
|
|
||||||
throw AssertionError(e)
|
|
||||||
}
|
|
||||||
else -> throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeSql(statement: SqlStatement) {
|
|
||||||
write(outputStream, BackupFrame.newBuilder().setStatement(statement).build())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writePreferenceEntry(preference: SharedPreference?) {
|
|
||||||
write(outputStream, BackupFrame.newBuilder().setPreference(preference).build())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeAvatar(avatarName: String, inputStream: InputStream, size: Long) {
|
|
||||||
write(outputStream, BackupFrame.newBuilder()
|
|
||||||
.setAvatar(Avatar.newBuilder()
|
|
||||||
.setName(avatarName)
|
|
||||||
.setLength(Util.toIntExact(size))
|
|
||||||
.build())
|
|
||||||
.build())
|
|
||||||
writeStream(inputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeAttachment(attachmentId: AttachmentId, inputStream: InputStream, size: Long) {
|
|
||||||
write(outputStream, BackupFrame.newBuilder()
|
|
||||||
.setAttachment(Attachment.newBuilder()
|
|
||||||
.setRowId(attachmentId.rowId)
|
|
||||||
.setAttachmentId(attachmentId.uniqueId)
|
|
||||||
.setLength(Util.toIntExact(size))
|
|
||||||
.build())
|
|
||||||
.build())
|
|
||||||
writeStream(inputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeSticker(rowId: Long, inputStream: InputStream, size: Long) {
|
|
||||||
write(outputStream, BackupFrame.newBuilder()
|
|
||||||
.setSticker(Sticker.newBuilder()
|
|
||||||
.setRowId(rowId)
|
|
||||||
.setLength(Util.toIntExact(size))
|
|
||||||
.build())
|
|
||||||
.build())
|
|
||||||
writeStream(inputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeDatabaseVersion(version: Int) {
|
|
||||||
write(outputStream, BackupFrame.newBuilder()
|
|
||||||
.setVersion(DatabaseVersion.newBuilder().setVersion(version))
|
|
||||||
.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeEnd() {
|
|
||||||
write(outputStream, BackupFrame.newBuilder().setEnd(true).build())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun writeStream(inputStream: InputStream) {
|
|
||||||
try {
|
|
||||||
Conversions.intToByteArray(iv, 0, counter++)
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
|
||||||
mac.update(iv)
|
|
||||||
val buffer = ByteArray(8192)
|
|
||||||
var read: Int
|
|
||||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
|
||||||
val ciphertext = cipher.update(buffer, 0, read)
|
|
||||||
if (ciphertext != null) {
|
|
||||||
outputStream.write(ciphertext)
|
|
||||||
mac.update(ciphertext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val remainder = cipher.doFinal()
|
|
||||||
outputStream.write(remainder)
|
|
||||||
mac.update(remainder)
|
|
||||||
val attachmentDigest = mac.doFinal()
|
|
||||||
outputStream.write(attachmentDigest, 0, 10)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is InvalidKeyException,
|
|
||||||
is InvalidAlgorithmParameterException,
|
|
||||||
is IllegalBlockSizeException,
|
|
||||||
is BadPaddingException -> {
|
|
||||||
throw AssertionError(e)
|
|
||||||
}
|
|
||||||
else -> throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun write(out: OutputStream, frame: BackupFrame) {
|
|
||||||
try {
|
|
||||||
Conversions.intToByteArray(iv, 0, counter++)
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
|
||||||
val frameCiphertext = cipher.doFinal(frame.toByteArray())
|
|
||||||
val frameMac = mac.doFinal(frameCiphertext)
|
|
||||||
val length = Conversions.intToByteArray(frameCiphertext.size + 10)
|
|
||||||
out.write(length)
|
|
||||||
out.write(frameCiphertext)
|
|
||||||
out.write(frameMac, 0, 10)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is InvalidKeyException,
|
|
||||||
is InvalidAlgorithmParameterException,
|
|
||||||
is IllegalBlockSizeException,
|
|
||||||
is BadPaddingException -> {
|
|
||||||
throw AssertionError(e)
|
|
||||||
}
|
|
||||||
else -> throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun flush() {
|
|
||||||
outputStream.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun close() {
|
|
||||||
outputStream.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,352 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.backup
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
import org.session.libsession.avatars.AvatarHelper
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
|
||||||
import org.session.libsession.utilities.Address
|
|
||||||
import org.session.libsession.utilities.Conversions
|
|
||||||
import org.session.libsession.utilities.Util
|
|
||||||
import org.session.libsignal.crypto.kdf.HKDFv3
|
|
||||||
import org.session.libsignal.utilities.ByteUtil
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
|
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
|
||||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
|
||||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.LinkedList
|
|
||||||
import java.util.Locale
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.Mac
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
object FullBackupImporter {
|
|
||||||
/**
|
|
||||||
* Because BackupProtos.SharedPreference was made only to serialize string values,
|
|
||||||
* we use these 3-char prefixes to explicitly cast the values before inserting to a preference file.
|
|
||||||
*/
|
|
||||||
const val PREF_PREFIX_TYPE_INT = "i__"
|
|
||||||
const val PREF_PREFIX_TYPE_BOOLEAN = "b__"
|
|
||||||
|
|
||||||
private val TAG = FullBackupImporter::class.java.simpleName
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@WorkerThread
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun importFromUri(context: Context,
|
|
||||||
attachmentSecret: AttachmentSecret,
|
|
||||||
db: SQLiteDatabase,
|
|
||||||
fileUri: Uri,
|
|
||||||
passphrase: String) {
|
|
||||||
|
|
||||||
val baseInputStream = context.contentResolver.openInputStream(fileUri)
|
|
||||||
?: throw IOException("Cannot open an input stream for the file URI: $fileUri")
|
|
||||||
|
|
||||||
var count = 0
|
|
||||||
try {
|
|
||||||
BackupRecordInputStream(baseInputStream, passphrase).use { inputStream ->
|
|
||||||
db.beginTransaction()
|
|
||||||
dropAllTables(db)
|
|
||||||
var frame: BackupFrame
|
|
||||||
while (!inputStream.readFrame().also { frame = it }.end) {
|
|
||||||
if (count++ % 100 == 0) EventBus.getDefault().post(BackupEvent.createProgress(count))
|
|
||||||
when {
|
|
||||||
frame.hasVersion() -> processVersion(db, frame.version)
|
|
||||||
frame.hasStatement() -> processStatement(db, frame.statement)
|
|
||||||
frame.hasPreference() -> processPreference(context, frame.preference)
|
|
||||||
frame.hasAttachment() -> processAttachment(context, attachmentSecret, db, frame.attachment, inputStream)
|
|
||||||
frame.hasAvatar() -> processAvatar(context, frame.avatar, inputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trimEntriesForExpiredMessages(context, db)
|
|
||||||
db.setTransactionSuccessful()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (db.inTransaction()) {
|
|
||||||
db.endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventBus.getDefault().post(BackupEvent.createFinished())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun processVersion(db: SQLiteDatabase, version: DatabaseVersion) {
|
|
||||||
if (version.version > db.version) {
|
|
||||||
throw DatabaseDowngradeException(db.version, version.version)
|
|
||||||
}
|
|
||||||
db.version = version.version
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processStatement(db: SQLiteDatabase, statement: SqlStatement) {
|
|
||||||
val isForSmsFtsSecretTable = statement.statement.contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_")
|
|
||||||
val isForMmsFtsSecretTable = statement.statement.contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_")
|
|
||||||
val isForSqliteSecretTable = statement.statement.toLowerCase(Locale.ENGLISH).startsWith("create table sqlite_")
|
|
||||||
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForSqliteSecretTable) {
|
|
||||||
Log.i(TAG, "Ignoring import for statement: " + statement.statement)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val parameters: MutableList<Any?> = LinkedList()
|
|
||||||
for (parameter in statement.parametersList) {
|
|
||||||
when {
|
|
||||||
parameter.hasStringParamter() -> parameters.add(parameter.stringParamter)
|
|
||||||
parameter.hasDoubleParameter() -> parameters.add(parameter.doubleParameter)
|
|
||||||
parameter.hasIntegerParameter() -> parameters.add(parameter.integerParameter)
|
|
||||||
parameter.hasBlobParameter() -> parameters.add(parameter.blobParameter.toByteArray())
|
|
||||||
parameter.hasNullparameter() -> parameters.add(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parameters.size > 0) {
|
|
||||||
db.execSQL(statement.statement, parameters.toTypedArray())
|
|
||||||
} else {
|
|
||||||
db.execSQL(statement.statement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun processAttachment(context: Context, attachmentSecret: AttachmentSecret,
|
|
||||||
db: SQLiteDatabase, attachment: Attachment,
|
|
||||||
inputStream: BackupRecordInputStream) {
|
|
||||||
val partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE)
|
|
||||||
val dataFile = File.createTempFile("part", ".mms", partsDirectory)
|
|
||||||
val output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false)
|
|
||||||
inputStream.readAttachmentTo(output.second, attachment.length)
|
|
||||||
val contentValues = ContentValues()
|
|
||||||
contentValues.put(AttachmentDatabase.DATA, dataFile.absolutePath)
|
|
||||||
contentValues.put(AttachmentDatabase.THUMBNAIL, null as String?)
|
|
||||||
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first)
|
|
||||||
db.update(AttachmentDatabase.TABLE_NAME, contentValues,
|
|
||||||
"${AttachmentDatabase.ROW_ID} = ? AND ${AttachmentDatabase.UNIQUE_ID} = ?",
|
|
||||||
arrayOf(attachment.rowId.toString(), attachment.attachmentId.toString()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun processAvatar(context: Context, avatar: Avatar, inputStream: BackupRecordInputStream) {
|
|
||||||
inputStream.readAttachmentTo(FileOutputStream(
|
|
||||||
AvatarHelper.getAvatarFile(context, Address.fromExternal(context, avatar.name))), avatar.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
|
||||||
private fun processPreference(context: Context, preference: SharedPreference) {
|
|
||||||
val preferences = context.getSharedPreferences(preference.file, 0)
|
|
||||||
val key = preference.key
|
|
||||||
val value = preference.value
|
|
||||||
|
|
||||||
// See the comment next to PREF_PREFIX_TYPE_* constants.
|
|
||||||
when {
|
|
||||||
key.startsWith(PREF_PREFIX_TYPE_INT) ->
|
|
||||||
preferences.edit().putInt(
|
|
||||||
key.substring(PREF_PREFIX_TYPE_INT.length),
|
|
||||||
value.toInt()
|
|
||||||
).commit()
|
|
||||||
key.startsWith(PREF_PREFIX_TYPE_BOOLEAN) ->
|
|
||||||
preferences.edit().putBoolean(
|
|
||||||
key.substring(PREF_PREFIX_TYPE_BOOLEAN.length),
|
|
||||||
value.toBoolean()
|
|
||||||
).commit()
|
|
||||||
else ->
|
|
||||||
preferences.edit().putString(key, value).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dropAllTables(db: SQLiteDatabase) {
|
|
||||||
db.rawQuery("SELECT name, type FROM sqlite_master", null).use { cursor ->
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
val name = cursor.getString(0)
|
|
||||||
val type = cursor.getString(1)
|
|
||||||
if ("table" == type && !name.startsWith("sqlite_")) {
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS $name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
|
|
||||||
val trimmedCondition = " NOT IN (SELECT ${MmsSmsColumns.ID} FROM ${MmsDatabase.TABLE_NAME})"
|
|
||||||
db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
|
|
||||||
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
|
|
||||||
val where = AttachmentDatabase.MMS_ID + trimmedCondition
|
|
||||||
db.query(AttachmentDatabase.TABLE_NAME, columns, where, null, null, null, null).use { cursor ->
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
DatabaseComponent.get(context).attachmentDatabase()
|
|
||||||
.deleteAttachment(AttachmentId(cursor.getLong(0), cursor.getLong(1)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
db.query(ThreadDatabase.TABLE_NAME, arrayOf(ThreadDatabase.ID),
|
|
||||||
ThreadDatabase.EXPIRES_IN + " > 0", null, null, null, null).use { cursor ->
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
DatabaseComponent.get(context).threadDatabase().update(cursor.getLong(0), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BackupRecordInputStream : Closeable {
|
|
||||||
private val inputStream: InputStream
|
|
||||||
private val cipher: Cipher
|
|
||||||
private val mac: Mac
|
|
||||||
private val cipherKey: ByteArray
|
|
||||||
private val macKey: ByteArray
|
|
||||||
private val iv: ByteArray
|
|
||||||
|
|
||||||
private var counter = 0
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
constructor(inputStream: InputStream, passphrase: String) : super() {
|
|
||||||
try {
|
|
||||||
this.inputStream = inputStream
|
|
||||||
val headerLengthBytes = ByteArray(4)
|
|
||||||
Util.readFully(this.inputStream, headerLengthBytes)
|
|
||||||
val headerLength = Conversions.byteArrayToInt(headerLengthBytes)
|
|
||||||
val headerFrame = ByteArray(headerLength)
|
|
||||||
Util.readFully(this.inputStream, headerFrame)
|
|
||||||
val frame = BackupFrame.parseFrom(headerFrame)
|
|
||||||
if (!frame.hasHeader()) {
|
|
||||||
throw IOException("Backup stream does not start with header!")
|
|
||||||
}
|
|
||||||
val header = frame.header
|
|
||||||
iv = header.iv.toByteArray()
|
|
||||||
if (iv.size != 16) {
|
|
||||||
throw IOException("Invalid IV length!")
|
|
||||||
}
|
|
||||||
val key = BackupUtil.computeBackupKey(passphrase, if (header.hasSalt()) header.salt.toByteArray() else null)
|
|
||||||
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
|
|
||||||
val split = ByteUtil.split(derived, 32, 32)
|
|
||||||
cipherKey = split[0]
|
|
||||||
macKey = split[1]
|
|
||||||
cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
|
||||||
mac = Mac.getInstance("HmacSHA256")
|
|
||||||
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
|
|
||||||
counter = Conversions.byteArrayToInt(iv)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is NoSuchAlgorithmException,
|
|
||||||
is NoSuchPaddingException,
|
|
||||||
is InvalidKeyException -> {
|
|
||||||
throw AssertionError(e)
|
|
||||||
}
|
|
||||||
else -> throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readFrame(): BackupFrame {
|
|
||||||
return readFrame(inputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readAttachmentTo(out: OutputStream, length: Int) {
|
|
||||||
var length = length
|
|
||||||
try {
|
|
||||||
Conversions.intToByteArray(iv, 0, counter++)
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
|
||||||
mac.update(iv)
|
|
||||||
val buffer = ByteArray(8192)
|
|
||||||
while (length > 0) {
|
|
||||||
val read = inputStream.read(buffer, 0, Math.min(buffer.size, length))
|
|
||||||
if (read == -1) throw IOException("File ended early!")
|
|
||||||
mac.update(buffer, 0, read)
|
|
||||||
val plaintext = cipher.update(buffer, 0, read)
|
|
||||||
if (plaintext != null) {
|
|
||||||
out.write(plaintext, 0, plaintext.size)
|
|
||||||
}
|
|
||||||
length -= read
|
|
||||||
}
|
|
||||||
val plaintext = cipher.doFinal()
|
|
||||||
if (plaintext != null) {
|
|
||||||
out.write(plaintext, 0, plaintext.size)
|
|
||||||
}
|
|
||||||
out.close()
|
|
||||||
val ourMac = ByteUtil.trim(mac.doFinal(), 10)
|
|
||||||
val theirMac = ByteArray(10)
|
|
||||||
try {
|
|
||||||
Util.readFully(inputStream, theirMac)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw IOException(e)
|
|
||||||
}
|
|
||||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
|
||||||
throw IOException("Bad MAC")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is InvalidKeyException,
|
|
||||||
is InvalidAlgorithmParameterException,
|
|
||||||
is IllegalBlockSizeException,
|
|
||||||
is BadPaddingException -> {
|
|
||||||
throw AssertionError(e)
|
|
||||||
}
|
|
||||||
else -> throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun readFrame(`in`: InputStream?): BackupFrame {
|
|
||||||
return try {
|
|
||||||
val length = ByteArray(4)
|
|
||||||
Util.readFully(`in`, length)
|
|
||||||
val frame = ByteArray(Conversions.byteArrayToInt(length))
|
|
||||||
Util.readFully(`in`, frame)
|
|
||||||
val theirMac = ByteArray(10)
|
|
||||||
System.arraycopy(frame, frame.size - 10, theirMac, 0, theirMac.size)
|
|
||||||
mac.update(frame, 0, frame.size - 10)
|
|
||||||
val ourMac = ByteUtil.trim(mac.doFinal(), 10)
|
|
||||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
|
||||||
throw IOException("Bad MAC")
|
|
||||||
}
|
|
||||||
Conversions.intToByteArray(iv, 0, counter++)
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
|
||||||
val plaintext = cipher.doFinal(frame, 0, frame.size - 10)
|
|
||||||
BackupFrame.parseFrom(plaintext)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is InvalidKeyException,
|
|
||||||
is InvalidAlgorithmParameterException,
|
|
||||||
is IllegalBlockSizeException,
|
|
||||||
is BadPaddingException -> {
|
|
||||||
throw AssertionError(e)
|
|
||||||
}
|
|
||||||
else -> throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun close() {
|
|
||||||
inputStream.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DatabaseDowngradeException internal constructor(currentVersion: Int, backupVersion: Int) :
|
|
||||||
IOException("Tried to import a backup with version $backupVersion into a database with version $currentVersion")
|
|
||||||
}
|
|
|
@ -249,17 +249,12 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||||
viewModel.callState.collect { state ->
|
viewModel.callState.collect { state ->
|
||||||
Log.d("Loki", "Consuming view model state $state")
|
Log.d("Loki", "Consuming view model state $state")
|
||||||
when (state) {
|
when (state) {
|
||||||
CALL_RINGING -> {
|
CALL_RINGING -> if (wantsToAnswer) {
|
||||||
if (wantsToAnswer) {
|
answerCall()
|
||||||
answerCall()
|
|
||||||
wantsToAnswer = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CALL_OUTGOING -> {
|
|
||||||
}
|
|
||||||
CALL_CONNECTED -> {
|
|
||||||
wantsToAnswer = false
|
wantsToAnswer = false
|
||||||
}
|
}
|
||||||
|
CALL_CONNECTED -> wantsToAnswer = false
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
updateControls(state)
|
updateControls(state)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.IdRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Stub;
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class AlbumThumbnailView extends FrameLayout {
|
|
||||||
|
|
||||||
private @Nullable SlideClickListener thumbnailClickListener;
|
|
||||||
private @Nullable SlidesClickedListener downloadClickListener;
|
|
||||||
|
|
||||||
private int currentSizeClass;
|
|
||||||
|
|
||||||
private ViewGroup albumCellContainer;
|
|
||||||
private Stub<TransferControlView> transferControls;
|
|
||||||
|
|
||||||
private final SlideClickListener defaultThumbnailClickListener = (v, slide) -> {
|
|
||||||
if (thumbnailClickListener != null) {
|
|
||||||
thumbnailClickListener.onClick(v, slide);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final OnLongClickListener defaultLongClickListener = v -> this.performLongClick();
|
|
||||||
|
|
||||||
public AlbumThumbnailView(@NonNull Context context) {
|
|
||||||
super(context);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public AlbumThumbnailView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
inflate(getContext(), R.layout.album_thumbnail_view, this);
|
|
||||||
|
|
||||||
albumCellContainer = findViewById(R.id.albumCellContainer);
|
|
||||||
transferControls = new Stub<>(findViewById(R.id.albumTransferControlsStub));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
|
|
||||||
if (slides.size() < 2) {
|
|
||||||
throw new IllegalStateException("Provided less than two slides.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showControls) {
|
|
||||||
transferControls.get().setShowDownloadText(true);
|
|
||||||
transferControls.get().setSlides(slides);
|
|
||||||
transferControls.get().setDownloadClickListener(v -> {
|
|
||||||
if (downloadClickListener != null) {
|
|
||||||
downloadClickListener.onClick(v, slides);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (transferControls.resolved()) {
|
|
||||||
transferControls.get().setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int sizeClass = Math.min(slides.size(), 6);
|
|
||||||
|
|
||||||
if (sizeClass != currentSizeClass) {
|
|
||||||
inflateLayout(sizeClass);
|
|
||||||
currentSizeClass = sizeClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
showSlides(glideRequests, slides);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCellBackgroundColor(@ColorInt int color) {
|
|
||||||
ViewGroup cellRoot = findViewById(R.id.album_thumbnail_root);
|
|
||||||
|
|
||||||
if (cellRoot != null) {
|
|
||||||
for (int i = 0; i < cellRoot.getChildCount(); i++) {
|
|
||||||
cellRoot.getChildAt(i).setBackgroundColor(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThumbnailClickListener(@Nullable SlideClickListener listener) {
|
|
||||||
thumbnailClickListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadClickListener(@Nullable SlidesClickedListener listener) {
|
|
||||||
downloadClickListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void inflateLayout(int sizeClass) {
|
|
||||||
albumCellContainer.removeAllViews();
|
|
||||||
|
|
||||||
switch (sizeClass) {
|
|
||||||
case 2:
|
|
||||||
inflate(getContext(), R.layout.album_thumbnail_2, albumCellContainer);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
inflate(getContext(), R.layout.album_thumbnail_3, albumCellContainer);
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
inflate(getContext(), R.layout.album_thumbnail_4, albumCellContainer);
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
inflate(getContext(), R.layout.album_thumbnail_5, albumCellContainer);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
inflate(getContext(), R.layout.album_thumbnail_many, albumCellContainer);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides) {
|
|
||||||
setSlide(glideRequests, slides.get(0), R.id.album_cell_1);
|
|
||||||
setSlide(glideRequests, slides.get(1), R.id.album_cell_2);
|
|
||||||
|
|
||||||
if (slides.size() >= 3) {
|
|
||||||
setSlide(glideRequests, slides.get(2), R.id.album_cell_3);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slides.size() >= 4) {
|
|
||||||
setSlide(glideRequests, slides.get(3), R.id.album_cell_4);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slides.size() >= 5) {
|
|
||||||
setSlide(glideRequests, slides.get(4), R.id.album_cell_5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slides.size() > 5) {
|
|
||||||
TextView text = findViewById(R.id.album_cell_overflow_text);
|
|
||||||
text.setText(getContext().getString(R.string.AlbumThumbnailView_plus, slides.size() - 5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,6 +13,7 @@ import android.widget.TextView;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.session.libsession.snode.SnodeAPI;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView;
|
import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
@ -106,7 +107,7 @@ public class ConversationItemFooter extends LinearLayout {
|
||||||
messageRecord.getExpiresIn());
|
messageRecord.getExpiresIn());
|
||||||
this.timerView.startAnimation();
|
this.timerView.startAnimation();
|
||||||
|
|
||||||
if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= System.currentTimeMillis()) {
|
if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= SnodeAPI.getNowWithOffset()) {
|
||||||
ApplicationContext.getInstance(getContext()).getExpiringMessageManager().checkSchedule();
|
ApplicationContext.getInstance(getContext()).getExpiringMessageManager().checkSchedule();
|
||||||
}
|
}
|
||||||
} else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
|
} else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
|
||||||
|
|
|
@ -1,158 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.UiThread;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class ConversationItemThumbnail extends FrameLayout {
|
|
||||||
|
|
||||||
private ThumbnailView thumbnail;
|
|
||||||
private AlbumThumbnailView album;
|
|
||||||
private ImageView shade;
|
|
||||||
private ConversationItemFooter footer;
|
|
||||||
private CornerMask cornerMask;
|
|
||||||
private Outliner outliner;
|
|
||||||
private boolean borderless;
|
|
||||||
|
|
||||||
public ConversationItemThumbnail(Context context) {
|
|
||||||
super(context);
|
|
||||||
init(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConversationItemThumbnail(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConversationItemThumbnail(final Context context, AttributeSet attrs, int defStyle) {
|
|
||||||
super(context, attrs, defStyle);
|
|
||||||
init(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init(@Nullable AttributeSet attrs) {
|
|
||||||
inflate(getContext(), R.layout.conversation_item_thumbnail, this);
|
|
||||||
|
|
||||||
this.thumbnail = findViewById(R.id.conversation_thumbnail_image);
|
|
||||||
this.album = findViewById(R.id.conversation_thumbnail_album);
|
|
||||||
this.shade = findViewById(R.id.conversation_thumbnail_shade);
|
|
||||||
this.footer = findViewById(R.id.conversation_thumbnail_footer);
|
|
||||||
this.cornerMask = new CornerMask(this);
|
|
||||||
this.outliner = new Outliner();
|
|
||||||
|
|
||||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
|
||||||
|
|
||||||
if (attrs != null) {
|
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
|
||||||
typedArray.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void dispatchDraw(Canvas canvas) {
|
|
||||||
super.dispatchDraw(canvas);
|
|
||||||
|
|
||||||
if (!borderless) {
|
|
||||||
cornerMask.mask(canvas);
|
|
||||||
|
|
||||||
if (album.getVisibility() != VISIBLE) {
|
|
||||||
outliner.draw(canvas);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setFocusable(boolean focusable) {
|
|
||||||
thumbnail.setFocusable(focusable);
|
|
||||||
album.setFocusable(focusable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClickable(boolean clickable) {
|
|
||||||
thumbnail.setClickable(clickable);
|
|
||||||
album.setClickable(clickable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
|
|
||||||
thumbnail.setOnLongClickListener(l);
|
|
||||||
album.setOnLongClickListener(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showShade(boolean show) {
|
|
||||||
shade.setVisibility(show ? VISIBLE : GONE);
|
|
||||||
forceLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
|
||||||
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
|
||||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBorderless(boolean borderless) {
|
|
||||||
this.borderless = borderless;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConversationItemFooter getFooter() {
|
|
||||||
return footer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides,
|
|
||||||
boolean showControls, boolean isPreview)
|
|
||||||
{
|
|
||||||
if (slides.size() == 1) {
|
|
||||||
thumbnail.setVisibility(VISIBLE);
|
|
||||||
album.setVisibility(GONE);
|
|
||||||
|
|
||||||
Slide slide = slides.get(0);
|
|
||||||
Attachment attachment = slide.asAttachment();
|
|
||||||
thumbnail.setImageResource(glideRequests, slide, showControls, isPreview, attachment.getWidth(), attachment.getHeight());
|
|
||||||
thumbnail.setLoadIndicatorVisibile(slide.isInProgress());
|
|
||||||
setTouchDelegate(thumbnail.getTouchDelegate());
|
|
||||||
} else {
|
|
||||||
thumbnail.setVisibility(GONE);
|
|
||||||
album.setVisibility(VISIBLE);
|
|
||||||
|
|
||||||
album.setSlides(glideRequests, slides, showControls);
|
|
||||||
setTouchDelegate(album.getTouchDelegate());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setConversationColor(@ColorInt int color) {
|
|
||||||
if (album.getVisibility() == VISIBLE) {
|
|
||||||
album.setCellBackgroundColor(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThumbnailClickListener(SlideClickListener listener) {
|
|
||||||
thumbnail.setThumbnailClickListener(listener);
|
|
||||||
album.setThumbnailClickListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadClickListener(SlidesClickedListener listener) {
|
|
||||||
thumbnail.setDownloadClickListener(listener);
|
|
||||||
album.setDownloadClickListener(listener);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,30 +4,48 @@ import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget;
|
import com.bumptech.glide.request.target.BitmapImageViewTarget;
|
||||||
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
import org.session.libsignal.utilities.SettableFuture;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
public class GlideBitmapListeningTarget extends BitmapImageViewTarget {
|
public class GlideBitmapListeningTarget extends BitmapImageViewTarget {
|
||||||
|
|
||||||
private final SettableFuture<Boolean> loaded;
|
private final SettableFuture<Boolean> loaded;
|
||||||
|
private final WeakReference<View> loadingView;
|
||||||
|
|
||||||
public GlideBitmapListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
|
public GlideBitmapListeningTarget(@NonNull ImageView view, @Nullable View loadingView, @NonNull SettableFuture<Boolean> loaded) {
|
||||||
super(view);
|
super(view);
|
||||||
this.loaded = loaded;
|
this.loaded = loaded;
|
||||||
|
this.loadingView = new WeakReference<View>(loadingView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setResource(@Nullable Bitmap resource) {
|
protected void setResource(@Nullable Bitmap resource) {
|
||||||
super.setResource(resource);
|
super.setResource(resource);
|
||||||
loaded.set(true);
|
loaded.set(true);
|
||||||
|
|
||||||
|
View loadingViewInstance = loadingView.get();
|
||||||
|
|
||||||
|
if (loadingViewInstance != null) {
|
||||||
|
loadingViewInstance.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||||
super.onLoadFailed(errorDrawable);
|
super.onLoadFailed(errorDrawable);
|
||||||
loaded.set(true);
|
loaded.set(true);
|
||||||
|
|
||||||
|
View loadingViewInstance = loadingView.get();
|
||||||
|
|
||||||
|
if (loadingViewInstance != null) {
|
||||||
|
loadingViewInstance.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,30 +3,48 @@ package org.thoughtcrime.securesms.components;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import com.bumptech.glide.request.target.DrawableImageViewTarget;
|
import com.bumptech.glide.request.target.DrawableImageViewTarget;
|
||||||
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
import org.session.libsignal.utilities.SettableFuture;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
public class GlideDrawableListeningTarget extends DrawableImageViewTarget {
|
public class GlideDrawableListeningTarget extends DrawableImageViewTarget {
|
||||||
|
|
||||||
private final SettableFuture<Boolean> loaded;
|
private final SettableFuture<Boolean> loaded;
|
||||||
|
private final WeakReference<View> loadingView;
|
||||||
|
|
||||||
public GlideDrawableListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
|
public GlideDrawableListeningTarget(@NonNull ImageView view, @Nullable View loadingView, @NonNull SettableFuture<Boolean> loaded) {
|
||||||
super(view);
|
super(view);
|
||||||
this.loaded = loaded;
|
this.loaded = loaded;
|
||||||
|
this.loadingView = new WeakReference<View>(loadingView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setResource(@Nullable Drawable resource) {
|
protected void setResource(@Nullable Drawable resource) {
|
||||||
super.setResource(resource);
|
super.setResource(resource);
|
||||||
loaded.set(true);
|
loaded.set(true);
|
||||||
|
|
||||||
|
View loadingViewInstance = loadingView.get();
|
||||||
|
|
||||||
|
if (loadingViewInstance != null) {
|
||||||
|
loadingViewInstance.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||||
super.onLoadFailed(errorDrawable);
|
super.onLoadFailed(errorDrawable);
|
||||||
loaded.set(true);
|
loaded.set(true);
|
||||||
|
|
||||||
|
View loadingViewInstance = loadingView.get();
|
||||||
|
|
||||||
|
if (loadingViewInstance != null) {
|
||||||
|
loadingViewInstance.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.components
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Path
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.RelativeLayout
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.ViewSeparatorBinding
|
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
|
||||||
|
|
||||||
class LabeledSeparatorView : RelativeLayout {
|
|
||||||
|
|
||||||
private lateinit var binding: ViewSeparatorBinding
|
|
||||||
private val path = Path()
|
|
||||||
|
|
||||||
private val paint: Paint by lazy {
|
|
||||||
val result = Paint()
|
|
||||||
result.style = Paint.Style.STROKE
|
|
||||||
result.color = ThemeUtil.getThemedColor(context, R.attr.dividerHorizontal)
|
|
||||||
result.strokeWidth = toPx(1, resources).toFloat()
|
|
||||||
result.isAntiAlias = true
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
constructor(context: Context) : super(context) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
|
||||||
binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
|
|
||||||
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
||||||
addView(binding.root, layoutParams)
|
|
||||||
setWillNotDraw(false)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Updating
|
|
||||||
override fun onDraw(c: Canvas) {
|
|
||||||
super.onDraw(c)
|
|
||||||
val w = width.toFloat()
|
|
||||||
val h = height.toFloat()
|
|
||||||
val hMargin = toPx(16, resources).toFloat()
|
|
||||||
path.reset()
|
|
||||||
path.moveTo(0.0f, h / 2)
|
|
||||||
path.lineTo(binding.titleTextView.left - hMargin, h / 2)
|
|
||||||
path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
|
|
||||||
path.moveTo(binding.titleTextView.right + hMargin, h / 2)
|
|
||||||
path.lineTo(w, h / 2)
|
|
||||||
path.close()
|
|
||||||
c.drawPath(path, paint)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
|
|
||||||
public class LinkPreviewView extends FrameLayout {
|
|
||||||
|
|
||||||
private static final int TYPE_CONVERSATION = 0;
|
|
||||||
private static final int TYPE_COMPOSE = 1;
|
|
||||||
|
|
||||||
private ViewGroup container;
|
|
||||||
private OutlinedThumbnailView thumbnail;
|
|
||||||
private TextView title;
|
|
||||||
private TextView site;
|
|
||||||
private View divider;
|
|
||||||
private View closeButton;
|
|
||||||
private View spinner;
|
|
||||||
|
|
||||||
private int type;
|
|
||||||
private int defaultRadius;
|
|
||||||
private CornerMask cornerMask;
|
|
||||||
private Outliner outliner;
|
|
||||||
private CloseClickedListener closeClickedListener;
|
|
||||||
|
|
||||||
public LinkPreviewView(Context context) {
|
|
||||||
super(context);
|
|
||||||
init(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LinkPreviewView(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init(@Nullable AttributeSet attrs) {
|
|
||||||
inflate(getContext(), R.layout.link_preview, this);
|
|
||||||
|
|
||||||
container = findViewById(R.id.linkpreview_container);
|
|
||||||
thumbnail = findViewById(R.id.linkpreview_thumbnail);
|
|
||||||
title = findViewById(R.id.linkpreview_title);
|
|
||||||
site = findViewById(R.id.linkpreview_site);
|
|
||||||
divider = findViewById(R.id.linkpreview_divider);
|
|
||||||
spinner = findViewById(R.id.linkpreview_progress_wheel);
|
|
||||||
closeButton = findViewById(R.id.linkpreview_close);
|
|
||||||
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
|
|
||||||
cornerMask = new CornerMask(this);
|
|
||||||
outliner = new Outliner();
|
|
||||||
|
|
||||||
outliner.setColor(getResources().getColor(R.color.transparent));
|
|
||||||
|
|
||||||
if (attrs != null) {
|
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
|
|
||||||
type = typedArray.getInt(R.styleable.LinkPreviewView_linkpreview_type, 0);
|
|
||||||
typedArray.recycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == TYPE_COMPOSE) {
|
|
||||||
container.setBackgroundColor(Color.TRANSPARENT);
|
|
||||||
container.setPadding(0, 0, 0, 0);
|
|
||||||
divider.setVisibility(VISIBLE);
|
|
||||||
|
|
||||||
closeButton.setOnClickListener(v -> {
|
|
||||||
if (closeClickedListener != null) {
|
|
||||||
closeClickedListener.onCloseClicked();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setWillNotDraw(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void dispatchDraw(Canvas canvas) {
|
|
||||||
super.dispatchDraw(canvas);
|
|
||||||
if (type == TYPE_COMPOSE) return;
|
|
||||||
|
|
||||||
cornerMask.mask(canvas);
|
|
||||||
outliner.draw(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLoading() {
|
|
||||||
title.setVisibility(GONE);
|
|
||||||
site.setVisibility(GONE);
|
|
||||||
thumbnail.setVisibility(GONE);
|
|
||||||
spinner.setVisibility(VISIBLE);
|
|
||||||
closeButton.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) {
|
|
||||||
setLinkPreview(glideRequests, linkPreview, showThumbnail);
|
|
||||||
if (showCloseButton) {
|
|
||||||
closeButton.setVisibility(VISIBLE);
|
|
||||||
} else {
|
|
||||||
closeButton.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
|
||||||
title.setVisibility(VISIBLE);
|
|
||||||
site.setVisibility(VISIBLE);
|
|
||||||
thumbnail.setVisibility(VISIBLE);
|
|
||||||
spinner.setVisibility(GONE);
|
|
||||||
closeButton.setVisibility(VISIBLE);
|
|
||||||
|
|
||||||
title.setText(linkPreview.getTitle());
|
|
||||||
|
|
||||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
|
||||||
if (url != null) {
|
|
||||||
site.setText(url.topPrivateDomain());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
|
||||||
thumbnail.setVisibility(VISIBLE);
|
|
||||||
thumbnail.setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
|
||||||
thumbnail.showDownloadText(false);
|
|
||||||
} else {
|
|
||||||
thumbnail.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCorners(int topLeft, int topRight) {
|
|
||||||
cornerMask.setRadii(topLeft, topRight, 0, 0);
|
|
||||||
outliner.setRadii(topLeft, topRight, 0, 0);
|
|
||||||
thumbnail.setCorners(topLeft, defaultRadius, defaultRadius, defaultRadius);
|
|
||||||
postInvalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCloseClickedListener(@Nullable CloseClickedListener closeClickedListener) {
|
|
||||||
this.closeClickedListener = closeClickedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadClickedListener(SlidesClickedListener listener) {
|
|
||||||
thumbnail.setDownloadClickListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface CloseClickedListener {
|
|
||||||
void onCloseClicked();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewConfiguration
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.sign
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
|
||||||
|
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
|
||||||
|
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
|
||||||
|
*
|
||||||
|
* This solution has limitations when using multiple levels of nested scrollable elements
|
||||||
|
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
|
||||||
|
*/
|
||||||
|
class NestedScrollableHost : FrameLayout {
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
|
|
||||||
|
private var touchSlop = 0
|
||||||
|
private var initialX = 0f
|
||||||
|
private var initialY = 0f
|
||||||
|
private val parentViewPager: ViewPager2?
|
||||||
|
get() {
|
||||||
|
var v: View? = parent as? View
|
||||||
|
while (v != null && v !is ViewPager2) {
|
||||||
|
v = v.parent as? View
|
||||||
|
}
|
||||||
|
return v as? ViewPager2
|
||||||
|
}
|
||||||
|
|
||||||
|
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
|
||||||
|
|
||||||
|
init {
|
||||||
|
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
|
||||||
|
val direction = -delta.sign.toInt()
|
||||||
|
return when (orientation) {
|
||||||
|
0 -> child?.canScrollHorizontally(direction) ?: false
|
||||||
|
1 -> child?.canScrollVertically(direction) ?: false
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
||||||
|
handleInterceptTouchEvent(e)
|
||||||
|
return super.onInterceptTouchEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInterceptTouchEvent(e: MotionEvent) {
|
||||||
|
val orientation = parentViewPager?.orientation ?: return
|
||||||
|
|
||||||
|
// Early return if child can't scroll in same direction as parent
|
||||||
|
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.action == MotionEvent.ACTION_DOWN) {
|
||||||
|
initialX = e.x
|
||||||
|
initialY = e.y
|
||||||
|
parent.requestDisallowInterceptTouchEvent(true)
|
||||||
|
} else if (e.action == MotionEvent.ACTION_MOVE) {
|
||||||
|
val dx = e.x - initialX
|
||||||
|
val dy = e.y - initialY
|
||||||
|
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
|
||||||
|
|
||||||
|
// assuming ViewPager2 touch-slop is 2x touch-slop of child
|
||||||
|
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
|
||||||
|
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
|
||||||
|
|
||||||
|
if (scaledDx > touchSlop || scaledDy > touchSlop) {
|
||||||
|
if (isVpHorizontal == (scaledDy > scaledDx)) {
|
||||||
|
// Gesture is perpendicular, allow all parents to intercept
|
||||||
|
parent.requestDisallowInterceptTouchEvent(false)
|
||||||
|
} else {
|
||||||
|
// Gesture is parallel, query child if movement in that direction is possible
|
||||||
|
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
|
||||||
|
// Child can scroll, disallow all parents to intercept
|
||||||
|
parent.requestDisallowInterceptTouchEvent(true)
|
||||||
|
} else {
|
||||||
|
// Child cannot scroll, allow all parents to intercept
|
||||||
|
parent.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,48 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class OutlinedThumbnailView extends ThumbnailView {
|
|
||||||
|
|
||||||
private CornerMask cornerMask;
|
|
||||||
private Outliner outliner;
|
|
||||||
|
|
||||||
public OutlinedThumbnailView(Context context) {
|
|
||||||
super(context);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public OutlinedThumbnailView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
cornerMask = new CornerMask(this);
|
|
||||||
outliner = new Outliner();
|
|
||||||
|
|
||||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
|
||||||
setWillNotDraw(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void dispatchDraw(Canvas canvas) {
|
|
||||||
super.dispatchDraw(canvas);
|
|
||||||
|
|
||||||
cornerMask.mask(canvas);
|
|
||||||
outliner.draw(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
|
||||||
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
|
||||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
|
||||||
postInvalidate();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.graphics.Path;
|
|
||||||
import android.graphics.RectF;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
|
|
||||||
public class Outliner {
|
|
||||||
|
|
||||||
private final float[] radii = new float[8];
|
|
||||||
private final Path corners = new Path();
|
|
||||||
private final RectF bounds = new RectF();
|
|
||||||
private final Paint outlinePaint = new Paint();
|
|
||||||
{
|
|
||||||
outlinePaint.setStyle(Paint.Style.STROKE);
|
|
||||||
outlinePaint.setStrokeWidth(1f);
|
|
||||||
outlinePaint.setAntiAlias(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColor(@ColorInt int color) {
|
|
||||||
outlinePaint.setColor(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void draw(Canvas canvas) {
|
|
||||||
final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
|
|
||||||
|
|
||||||
bounds.left = halfStrokeWidth;
|
|
||||||
bounds.top = halfStrokeWidth;
|
|
||||||
bounds.right = canvas.getWidth() - halfStrokeWidth;
|
|
||||||
bounds.bottom = canvas.getHeight() - halfStrokeWidth;
|
|
||||||
|
|
||||||
corners.reset();
|
|
||||||
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
|
||||||
|
|
||||||
canvas.drawPath(corners, outlinePaint);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRadius(int radius) {
|
|
||||||
setRadii(radius, radius, radius, radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
|
||||||
radii[0] = radii[1] = topLeft;
|
|
||||||
radii[2] = radii[3] = topRight;
|
|
||||||
radii[4] = radii[5] = bottomRight;
|
|
||||||
radii[6] = radii[7] = bottomLeft;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
|
@ -15,15 +16,17 @@ import org.session.libsession.avatars.ProfileContactPhoto
|
||||||
import org.session.libsession.avatars.ResourceContactPhoto
|
import org.session.libsession.avatars.ResourceContactPhoto
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class ProfilePictureView @JvmOverloads constructor(
|
class ProfilePictureView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null
|
context: Context, attrs: AttributeSet? = null
|
||||||
) : RelativeLayout(context, attrs) {
|
) : RelativeLayout(context, attrs) {
|
||||||
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
|
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
||||||
lateinit var glide: GlideRequests
|
private val glide: GlideRequests = GlideApp.with(this)
|
||||||
var publicKey: String? = null
|
var publicKey: String? = null
|
||||||
var displayName: String? = null
|
var displayName: String? = null
|
||||||
var additionalPublicKey: String? = null
|
var additionalPublicKey: String? = null
|
||||||
|
@ -31,32 +34,49 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||||
var isLarge = false
|
var isLarge = false
|
||||||
|
|
||||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
private val profilePicturesCache = mutableMapOf<String, String?>()
|
||||||
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
|
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
||||||
|
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
||||||
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
||||||
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
constructor(context: Context, sender: Recipient): this(context) {
|
||||||
|
update(sender)
|
||||||
|
}
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun update(recipient: Recipient) {
|
fun update(recipient: Recipient) {
|
||||||
fun getUserDisplayName(publicKey: String): String {
|
fun getUserDisplayName(publicKey: String): String {
|
||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
||||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||||
}
|
}
|
||||||
fun isOpenGroupWithProfilePicture(recipient: Recipient): Boolean {
|
|
||||||
return recipient.isOpenGroupRecipient && recipient.groupAvatarId != null
|
if (recipient.isClosedGroupRecipient) {
|
||||||
}
|
|
||||||
if (recipient.isGroupRecipient && !isOpenGroupWithProfilePicture(recipient)) {
|
|
||||||
val members = DatabaseComponent.get(context).groupDatabase()
|
val members = DatabaseComponent.get(context).groupDatabase()
|
||||||
.getGroupMemberAddresses(recipient.address.toGroupString(), true)
|
.getGroupMemberAddresses(recipient.address.toGroupString(), true)
|
||||||
.sorted()
|
.sorted()
|
||||||
.take(2)
|
.take(2)
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
val pk = members.getOrNull(0)?.serialize() ?: ""
|
if (members.size <= 1) {
|
||||||
publicKey = pk
|
publicKey = ""
|
||||||
displayName = getUserDisplayName(pk)
|
displayName = ""
|
||||||
val apk = members.getOrNull(1)?.serialize() ?: ""
|
additionalPublicKey = ""
|
||||||
additionalPublicKey = apk
|
additionalDisplayName = ""
|
||||||
additionalDisplayName = getUserDisplayName(apk)
|
} else {
|
||||||
|
val pk = members.getOrNull(0)?.serialize() ?: ""
|
||||||
|
publicKey = pk
|
||||||
|
displayName = getUserDisplayName(pk)
|
||||||
|
val apk = members.getOrNull(1)?.serialize() ?: ""
|
||||||
|
additionalPublicKey = apk
|
||||||
|
additionalDisplayName = getUserDisplayName(apk)
|
||||||
|
}
|
||||||
|
} else if(recipient.isOpenGroupInboxRecipient) {
|
||||||
|
val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
|
||||||
|
this.publicKey = publicKey
|
||||||
|
displayName = getUserDisplayName(publicKey)
|
||||||
|
additionalPublicKey = null
|
||||||
} else {
|
} else {
|
||||||
val publicKey = recipient.address.toString()
|
val publicKey = recipient.address.toString()
|
||||||
this.publicKey = publicKey
|
this.publicKey = publicKey
|
||||||
|
@ -67,7 +87,6 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update() {
|
fun update() {
|
||||||
if (!this::glide.isInitialized) return
|
|
||||||
val publicKey = publicKey ?: return
|
val publicKey = publicKey ?: return
|
||||||
val additionalPublicKey = additionalPublicKey
|
val additionalPublicKey = additionalPublicKey
|
||||||
if (additionalPublicKey != null) {
|
if (additionalPublicKey != null) {
|
||||||
|
@ -101,26 +120,37 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||||
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
||||||
val signalProfilePicture = recipient.contactPhoto
|
val signalProfilePicture = recipient.contactPhoto
|
||||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||||
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
|
||||||
|
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||||
|
|
||||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(signalProfilePicture)
|
glide.load(signalProfilePicture)
|
||||||
.placeholder(unknownRecipientDrawable)
|
.placeholder(unknownRecipientDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.error(unknownRecipientDrawable)
|
.error(glide.load(placeholder))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
|
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
|
||||||
|
glide.clear(imageView)
|
||||||
|
glide.load(unknownOpenGroupDrawable)
|
||||||
|
.centerCrop()
|
||||||
|
.circleCrop()
|
||||||
|
.into(imageView)
|
||||||
} else {
|
} else {
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(placeholder)
|
glide.load(placeholder)
|
||||||
.placeholder(unknownRecipientDrawable)
|
.placeholder(unknownRecipientDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
|
.circleCrop()
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
||||||
}
|
}
|
||||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
profilePicturesCache[publicKey] = recipient.profileAvatar
|
||||||
} else {
|
} else {
|
||||||
imageView.setImageDrawable(null)
|
glide.load(unknownRecipientDrawable)
|
||||||
|
.centerCrop()
|
||||||
|
.into(imageView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,304 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.contacts.Contact;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
|
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class QuoteView extends FrameLayout implements RecipientModifiedListener {
|
|
||||||
|
|
||||||
private static final String TAG = QuoteView.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final int MESSAGE_TYPE_PREVIEW = 0;
|
|
||||||
private static final int MESSAGE_TYPE_OUTGOING = 1;
|
|
||||||
private static final int MESSAGE_TYPE_INCOMING = 2;
|
|
||||||
|
|
||||||
private ViewGroup mainView;
|
|
||||||
private ViewGroup footerView;
|
|
||||||
private TextView authorView;
|
|
||||||
private TextView bodyView;
|
|
||||||
private ImageView quoteBarView;
|
|
||||||
private ImageView thumbnailView;
|
|
||||||
private View attachmentVideoOverlayView;
|
|
||||||
private ViewGroup attachmentContainerView;
|
|
||||||
private TextView attachmentNameView;
|
|
||||||
private ImageView dismissView;
|
|
||||||
|
|
||||||
private long id;
|
|
||||||
private Recipient author;
|
|
||||||
private String body;
|
|
||||||
private Recipient conversationRecipient;
|
|
||||||
private TextView mediaDescriptionText;
|
|
||||||
private TextView missingLinkText;
|
|
||||||
private SlideDeck attachments;
|
|
||||||
private int messageType;
|
|
||||||
private int largeCornerRadius;
|
|
||||||
private int smallCornerRadius;
|
|
||||||
private CornerMask cornerMask;
|
|
||||||
|
|
||||||
|
|
||||||
public QuoteView(Context context) {
|
|
||||||
super(context);
|
|
||||||
initialize(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public QuoteView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
initialize(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public QuoteView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
initialize(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public QuoteView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
initialize(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize(@Nullable AttributeSet attrs) {
|
|
||||||
inflate(getContext(), R.layout.quote_view, this);
|
|
||||||
|
|
||||||
this.mainView = findViewById(R.id.quote_main);
|
|
||||||
this.footerView = findViewById(R.id.quote_missing_footer);
|
|
||||||
this.authorView = findViewById(R.id.quote_author);
|
|
||||||
this.bodyView = findViewById(R.id.quote_text);
|
|
||||||
this.quoteBarView = findViewById(R.id.quote_bar);
|
|
||||||
this.thumbnailView = findViewById(R.id.quote_thumbnail);
|
|
||||||
this.attachmentVideoOverlayView = findViewById(R.id.quote_video_overlay);
|
|
||||||
this.attachmentContainerView = findViewById(R.id.quote_attachment_container);
|
|
||||||
this.attachmentNameView = findViewById(R.id.quote_attachment_name);
|
|
||||||
this.dismissView = findViewById(R.id.quote_dismiss);
|
|
||||||
this.mediaDescriptionText = findViewById(R.id.media_type);
|
|
||||||
this.missingLinkText = findViewById(R.id.quote_missing_text);
|
|
||||||
this.largeCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom);
|
|
||||||
this.smallCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom);
|
|
||||||
|
|
||||||
cornerMask = new CornerMask(this);
|
|
||||||
cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius);
|
|
||||||
|
|
||||||
if (attrs != null) {
|
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
|
|
||||||
int primaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorPrimary, Color.BLACK);
|
|
||||||
int secondaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorSecondary, Color.BLACK);
|
|
||||||
messageType = typedArray.getInt(R.styleable.QuoteView_message_type, 0);
|
|
||||||
typedArray.recycle();
|
|
||||||
|
|
||||||
dismissView.setVisibility(messageType == MESSAGE_TYPE_PREVIEW ? VISIBLE : GONE);
|
|
||||||
|
|
||||||
authorView.setTextColor(primaryColor);
|
|
||||||
bodyView.setTextColor(primaryColor);
|
|
||||||
attachmentNameView.setTextColor(primaryColor);
|
|
||||||
mediaDescriptionText.setTextColor(secondaryColor);
|
|
||||||
missingLinkText.setTextColor(primaryColor);
|
|
||||||
|
|
||||||
if (messageType == MESSAGE_TYPE_PREVIEW) {
|
|
||||||
int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
|
|
||||||
cornerMask.setTopLeftRadius(radius);
|
|
||||||
cornerMask.setTopRightRadius(radius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dismissView.setOnClickListener(view -> setVisibility(GONE));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void dispatchDraw(Canvas canvas) {
|
|
||||||
super.dispatchDraw(canvas);
|
|
||||||
cornerMask.mask(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuote(GlideRequests glideRequests,
|
|
||||||
long id,
|
|
||||||
@NonNull Recipient author,
|
|
||||||
@Nullable String body,
|
|
||||||
boolean originalMissing,
|
|
||||||
@NonNull SlideDeck attachments,
|
|
||||||
@NonNull Recipient conversationRecipient)
|
|
||||||
{
|
|
||||||
if (this.author != null) this.author.removeListener(this);
|
|
||||||
|
|
||||||
this.id = id;
|
|
||||||
this.author = author;
|
|
||||||
this.body = body;
|
|
||||||
this.attachments = attachments;
|
|
||||||
this.conversationRecipient = conversationRecipient;
|
|
||||||
|
|
||||||
author.addListener(this);
|
|
||||||
setQuoteAuthor(author);
|
|
||||||
setQuoteText(body, attachments);
|
|
||||||
setQuoteAttachment(glideRequests, attachments);
|
|
||||||
setQuoteMissingFooter(originalMissing);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTopCornerSizes(boolean topLeftLarge, boolean topRightLarge) {
|
|
||||||
cornerMask.setTopLeftRadius(topLeftLarge ? largeCornerRadius : smallCornerRadius);
|
|
||||||
cornerMask.setTopRightRadius(topRightLarge ? largeCornerRadius : smallCornerRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dismiss() {
|
|
||||||
if (this.author != null) this.author.removeListener(this);
|
|
||||||
|
|
||||||
this.id = 0;
|
|
||||||
this.author = null;
|
|
||||||
this.body = null;
|
|
||||||
|
|
||||||
setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onModified(Recipient recipient) {
|
|
||||||
Util.runOnMain(() -> {
|
|
||||||
if (recipient == author) {
|
|
||||||
setQuoteAuthor(recipient);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setQuoteAuthor(@NonNull Recipient author) {
|
|
||||||
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
|
||||||
boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress().serialize());
|
|
||||||
|
|
||||||
String quoteeDisplayName;
|
|
||||||
|
|
||||||
String senderHexEncodedPublicKey = author.getAddress().serialize();
|
|
||||||
if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
|
|
||||||
quoteeDisplayName = TextSecurePreferences.getProfileName(getContext());
|
|
||||||
} else {
|
|
||||||
SessionContactDatabase contactDB = DatabaseComponent.get(getContext()).sessionContactDatabase();
|
|
||||||
Contact contact = contactDB.getContactWithSessionID(senderHexEncodedPublicKey);
|
|
||||||
if (contact != null) {
|
|
||||||
Contact.ContactContext context = (this.conversationRecipient.isOpenGroupRecipient()) ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR;
|
|
||||||
quoteeDisplayName = contact.displayName(context);
|
|
||||||
} else {
|
|
||||||
quoteeDisplayName = senderHexEncodedPublicKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
authorView.setText(isOwnNumber ? getContext().getString(R.string.QuoteView_you) : quoteeDisplayName);
|
|
||||||
|
|
||||||
// We use the raw color resource because Android 4.x was struggling with tints here
|
|
||||||
int colorID = UiModeUtilities.isDayUiMode(getContext()) ? R.color.black : R.color.accent;
|
|
||||||
quoteBarView.setImageResource(colorID);
|
|
||||||
mainView.setBackgroundColor(ThemeUtil.getThemedColor(getContext(),
|
|
||||||
outgoing ? R.attr.message_received_background_color : R.attr.message_sent_background_color));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
|
|
||||||
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
|
|
||||||
bodyView.setVisibility(VISIBLE);
|
|
||||||
bodyView.setText(body == null ? "" : body);
|
|
||||||
mediaDescriptionText.setVisibility(GONE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bodyView.setVisibility(GONE);
|
|
||||||
mediaDescriptionText.setVisibility(VISIBLE);
|
|
||||||
|
|
||||||
List<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
|
|
||||||
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
|
|
||||||
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
|
|
||||||
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
|
|
||||||
|
|
||||||
// Given that most types have images, we specifically check images last
|
|
||||||
if (!audioSlides.isEmpty()) {
|
|
||||||
mediaDescriptionText.setText(R.string.QuoteView_audio);
|
|
||||||
} else if (!documentSlides.isEmpty()) {
|
|
||||||
mediaDescriptionText.setVisibility(GONE);
|
|
||||||
} else if (!videoSlides.isEmpty()) {
|
|
||||||
mediaDescriptionText.setText(R.string.QuoteView_video);
|
|
||||||
} else if (!imageSlides.isEmpty()) {
|
|
||||||
mediaDescriptionText.setText(R.string.QuoteView_photo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
|
|
||||||
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo()).limit(1).toList();
|
|
||||||
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
|
|
||||||
|
|
||||||
attachmentVideoOverlayView.setVisibility(GONE);
|
|
||||||
|
|
||||||
if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) {
|
|
||||||
thumbnailView.setVisibility(VISIBLE);
|
|
||||||
attachmentContainerView.setVisibility(GONE);
|
|
||||||
dismissView.setBackgroundResource(R.drawable.dismiss_background);
|
|
||||||
if (imageVideoSlides.get(0).hasVideo()) {
|
|
||||||
attachmentVideoOverlayView.setVisibility(VISIBLE);
|
|
||||||
}
|
|
||||||
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
|
|
||||||
.centerCrop()
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.into(thumbnailView);
|
|
||||||
} else if (!documentSlides.isEmpty()){
|
|
||||||
thumbnailView.setVisibility(GONE);
|
|
||||||
attachmentContainerView.setVisibility(VISIBLE);
|
|
||||||
attachmentNameView.setText(documentSlides.get(0).getFileName().or(""));
|
|
||||||
} else {
|
|
||||||
thumbnailView.setVisibility(GONE);
|
|
||||||
attachmentContainerView.setVisibility(GONE);
|
|
||||||
dismissView.setBackgroundDrawable(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ThemeUtil.isDarkTheme(getContext())) {
|
|
||||||
dismissView.setBackgroundResource(R.drawable.circle_alpha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setQuoteMissingFooter(boolean missing) {
|
|
||||||
footerView.setVisibility(missing ? VISIBLE : GONE);
|
|
||||||
footerView.setBackgroundColor(getResources().getColor(R.color.quote_not_found_background));
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getQuoteId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Recipient getAuthor() {
|
|
||||||
return author;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBody() {
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Attachment> getAttachments() {
|
|
||||||
return attachments.asAttachments();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package org.thoughtcrime.securesms.components
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import androidx.viewpager.widget.ViewPager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extension of ViewPager to swallow erroneous multi-touch exceptions.
|
||||||
|
*
|
||||||
|
* @see https://stackoverflow.com/questions/6919292/pointerindex-out-of-range-android-multitouch
|
||||||
|
*/
|
||||||
|
class SafeViewPager @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null
|
||||||
|
) : ViewPager(context, attrs) {
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onTouchEvent(event: MotionEvent?): Boolean = try {
|
||||||
|
super.onTouchEvent(event)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean = try {
|
||||||
|
super.onInterceptTouchEvent(event)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,19 +52,4 @@ public class StickerView extends FrameLayout {
|
||||||
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
|
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
|
||||||
image.setOnLongClickListener(l);
|
image.setOnLongClickListener(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
|
|
||||||
boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
|
|
||||||
|
|
||||||
image.setImageResource(glideRequests, stickerSlide, showControls, false);
|
|
||||||
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThumbnailClickListener(@NonNull SlideClickListener listener) {
|
|
||||||
image.setThumbnailClickListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
|
|
||||||
image.setDownloadClickListener(listener);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import androidx.preference.CheckBoxPreference;
|
import androidx.preference.CheckBoxPreference;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
|
@ -18,7 +17,6 @@ public class SwitchPreferenceCompat extends CheckBoxPreference {
|
||||||
setLayoutRes();
|
setLayoutRes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
setLayoutRes();
|
setLayoutRes();
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.AttrRes;
|
import androidx.annotation.AttrRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.Util;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class CompositeEmojiPageModel implements EmojiPageModel {
|
public class CompositeEmojiPageModel implements EmojiPageModel {
|
||||||
@AttrRes private final int iconAttr;
|
@AttrRes private final int iconAttr;
|
||||||
@NonNull private final EmojiPageModel[] models;
|
@NonNull private final List<EmojiPageModel> models;
|
||||||
|
|
||||||
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull EmojiPageModel... models) {
|
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List<EmojiPageModel> models) {
|
||||||
this.iconAttr = iconAttr;
|
this.iconAttr = iconAttr;
|
||||||
this.models = models;
|
this.models = models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKey() {
|
||||||
|
return Util.hasItems(models) ? models.get(0).getKey() : "";
|
||||||
|
}
|
||||||
|
|
||||||
public int getIconAttr() {
|
public int getIconAttr() {
|
||||||
return iconAttr;
|
return iconAttr;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +53,7 @@ public class CompositeEmojiPageModel implements EmojiPageModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable String getSprite() {
|
public @Nullable Uri getSpriteUri() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Emoji {
|
public class Emoji {
|
||||||
|
|
||||||
private final List<String> variations;
|
private final List<String> variations;
|
||||||
|
private final List<String> rawVariations;
|
||||||
|
|
||||||
public Emoji(String... variations) {
|
public Emoji(String... variations) {
|
||||||
this.variations = Arrays.asList(variations);
|
this(Arrays.asList(variations), Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Emoji(List<String> variations) {
|
||||||
|
this(variations, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Emoji(List<String> variations, List<String> rawVariations) {
|
||||||
|
this.variations = variations;
|
||||||
|
this.rawVariations = rawVariations;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getValue() {
|
public String getValue() {
|
||||||
|
@ -18,4 +31,15 @@ public class Emoji {
|
||||||
public List<String> getVariations() {
|
public List<String> getVariations() {
|
||||||
return variations;
|
return variations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasMultipleVariations() {
|
||||||
|
return variations.size() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getRawVariation(int variationIndex) {
|
||||||
|
if (rawVariations != null && variationIndex >= 0 && variationIndex < rawVariations.size()) {
|
||||||
|
return rawVariations.get(variationIndex);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.widget.AppCompatEditText;
|
|
||||||
import android.text.InputFilter;
|
import android.text.InputFilter;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.widget.AppCompatEditText;
|
||||||
|
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
|
|
||||||
public class EmojiEditText extends AppCompatEditText {
|
public class EmojiEditText extends AppCompatEditText {
|
||||||
private static final String TAG = EmojiEditText.class.getSimpleName();
|
private static final String TAG = Log.tag(EmojiEditText.class);
|
||||||
|
|
||||||
public EmojiEditText(Context context) {
|
public EmojiEditText(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
|
@ -26,8 +29,14 @@ public class EmojiEditText extends AppCompatEditText {
|
||||||
|
|
||||||
public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
|
public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
if (!TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
|
|
||||||
setFilters(appendEmojiFilter(this.getFilters()));
|
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
||||||
|
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||||
|
boolean jumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
|
||||||
|
a.recycle();
|
||||||
|
|
||||||
|
if (!isInEditMode() && forceCustom) {
|
||||||
|
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +54,7 @@ public class EmojiEditText extends AppCompatEditText {
|
||||||
else super.invalidateDrawable(drawable);
|
else super.invalidateDrawable(drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters) {
|
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
|
||||||
InputFilter[] result;
|
InputFilter[] result;
|
||||||
|
|
||||||
if (originalFilters != null) {
|
if (originalFilters != null) {
|
||||||
|
@ -55,7 +64,7 @@ public class EmojiEditText extends AppCompatEditText {
|
||||||
result = new InputFilter[1];
|
result = new InputFilter[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
result[0] = new EmojiFilter(this);
|
result[0] = new EmojiFilter(this, jumboEmoji);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
public interface EmojiEventListener {
|
||||||
|
void onEmojiSelected(String emoji);
|
||||||
|
|
||||||
|
void onKeyEvent(KeyEvent keyEvent);
|
||||||
|
}
|
|
@ -8,9 +8,11 @@ import android.widget.TextView;
|
||||||
|
|
||||||
public class EmojiFilter implements InputFilter {
|
public class EmojiFilter implements InputFilter {
|
||||||
private TextView view;
|
private TextView view;
|
||||||
|
private boolean jumboEmoji;
|
||||||
|
|
||||||
public EmojiFilter(TextView view) {
|
public EmojiFilter(TextView view, boolean jumboEmoji) {
|
||||||
this.view = view;
|
this.view = view;
|
||||||
|
this.jumboEmoji = jumboEmoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -19,7 +21,7 @@ public class EmojiFilter implements InputFilter {
|
||||||
char[] v = new char[end - start];
|
char[] v = new char[end - start];
|
||||||
TextUtils.getChars(source, start, end, v, 0);
|
TextUtils.getChars(source, start, end, v, 0);
|
||||||
|
|
||||||
Spannable emojified = EmojiProvider.getInstance(view.getContext()).emojify(new String(v), view);
|
Spannable emojified = EmojiProvider.emojify(new String(v), view, jumboEmoji);
|
||||||
|
|
||||||
if (source instanceof Spanned && emojified != null) {
|
if (source instanceof Spanned && emojified != null) {
|
||||||
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);
|
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
|
public class EmojiImageView extends AppCompatImageView {
|
||||||
|
|
||||||
|
private final boolean forceJumboEmoji;
|
||||||
|
|
||||||
|
public EmojiImageView(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmojiImageView(Context context, AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmojiImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiImageView, 0, 0);
|
||||||
|
forceJumboEmoji = a.getBoolean(R.styleable.EmojiImageView_forceJumbo, false);
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageEmoji(CharSequence emoji) {
|
||||||
|
if (isInEditMode()) {
|
||||||
|
setImageResource(R.drawable.ic_emoji);
|
||||||
|
} else {
|
||||||
|
Drawable emojiDrawable = EmojiProvider.getEmojiDrawable(getContext(), emoji);
|
||||||
|
if (emojiDrawable == null) {
|
||||||
|
// fallback
|
||||||
|
setImageResource(R.drawable.ic_outline_disabled_by_default_24);
|
||||||
|
} else {
|
||||||
|
setImageDrawable(emojiDrawable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.thoughtcrime.securesms.components.emoji
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiModel
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
|
||||||
|
import org.thoughtcrime.securesms.util.InsetItemDecoration
|
||||||
|
|
||||||
|
private val EDGE_LENGTH: Int = ViewUtil.dpToPx(6)
|
||||||
|
private val HORIZONTAL_INSET: Int = ViewUtil.dpToPx(6)
|
||||||
|
private val EMOJI_VERTICAL_INSET: Int = ViewUtil.dpToPx(5)
|
||||||
|
private val HEADER_VERTICAL_INSET: Int = ViewUtil.dpToPx(8)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use super class to add insets to the emojis and use the [onDrawOver] to draw the variation
|
||||||
|
* hint if the emoji has more than one variation.
|
||||||
|
*/
|
||||||
|
class EmojiItemDecoration(private val allowVariations: Boolean, private val variationsDrawable: Drawable) : InsetItemDecoration(SetInset()) {
|
||||||
|
|
||||||
|
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
super.onDrawOver(canvas, parent, state)
|
||||||
|
|
||||||
|
val adapter: EmojiPageViewGridAdapter? = parent.adapter as? EmojiPageViewGridAdapter
|
||||||
|
if (allowVariations && adapter != null) {
|
||||||
|
for (i in 0 until parent.childCount) {
|
||||||
|
val child: View = parent.getChildAt(i)
|
||||||
|
val position: Int = parent.getChildAdapterPosition(child)
|
||||||
|
if (position >= 0 && position <= adapter.itemCount) {
|
||||||
|
val model = adapter.currentList[position]
|
||||||
|
if (model is EmojiModel && model.emoji.hasMultipleVariations()) {
|
||||||
|
variationsDrawable.setBounds(child.right, child.bottom - EDGE_LENGTH, child.right + EDGE_LENGTH, child.bottom)
|
||||||
|
variationsDrawable.draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SetInset : InsetItemDecoration.SetInset() {
|
||||||
|
override fun setInset(outRect: Rect, view: View, parent: RecyclerView) {
|
||||||
|
val isHeader = view.javaClass == AppCompatTextView::class.java
|
||||||
|
|
||||||
|
outRect.left = HORIZONTAL_INSET
|
||||||
|
outRect.right = HORIZONTAL_INSET
|
||||||
|
outRect.top = if (isHeader) HEADER_VERTICAL_INSET else EMOJI_VERTICAL_INSET
|
||||||
|
outRect.bottom = if (isHeader) 0 else EMOJI_VERTICAL_INSET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -136,8 +136,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||||
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener);
|
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, false);
|
||||||
page.setModel(pages.get(position));
|
|
||||||
container.addView(page);
|
container.addView(page);
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
@ -160,8 +159,4 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface EmojiEventListener {
|
|
||||||
void onEmojiSelected(String emoji);
|
|
||||||
void onKeyEvent(KeyEvent keyEvent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface EmojiPageModel {
|
public interface EmojiPageModel {
|
||||||
|
String getKey();
|
||||||
int getIconAttr();
|
int getIconAttr();
|
||||||
List<String> getEmoji();
|
List<String> getEmoji();
|
||||||
List<Emoji> getDisplayEmoji();
|
List<Emoji> getDisplayEmoji();
|
||||||
boolean hasSpriteMap();
|
boolean hasSpriteMap();
|
||||||
String getSprite();
|
@Nullable Uri getSpriteUri();
|
||||||
boolean isDynamic();
|
boolean isDynamic();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +1,136 @@
|
||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
import android.graphics.drawable.Drawable;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import android.util.AttributeSet;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.FrameLayout;
|
|
||||||
|
|
||||||
|
import androidx.annotation.LayoutRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiHeader;
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiNoResultsModel;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class EmojiPageView extends FrameLayout implements VariationSelectorListener {
|
public class EmojiPageView extends RecyclerView implements VariationSelectorListener {
|
||||||
private static final String TAG = EmojiPageView.class.getSimpleName();
|
private AdapterFactory adapterFactory;
|
||||||
|
private LinearLayoutManager layoutManager;
|
||||||
private EmojiPageModel model;
|
|
||||||
private EmojiPageViewGridAdapter adapter;
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private GridLayoutManager layoutManager;
|
|
||||||
private RecyclerView.OnItemTouchListener scrollDisabler;
|
private RecyclerView.OnItemTouchListener scrollDisabler;
|
||||||
private VariationSelectorListener variationSelectorListener;
|
private VariationSelectorListener variationSelectorListener;
|
||||||
private EmojiVariationSelectorPopup popup;
|
private EmojiVariationSelectorPopup popup;
|
||||||
|
|
||||||
|
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
public EmojiPageView(@NonNull Context context,
|
public EmojiPageView(@NonNull Context context,
|
||||||
@NonNull EmojiKeyboardProvider.EmojiEventListener emojiSelectionListener,
|
@NonNull EmojiEventListener emojiSelectionListener,
|
||||||
@NonNull VariationSelectorListener variationSelectorListener)
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
|
boolean allowVariations)
|
||||||
{
|
{
|
||||||
super(context);
|
super(context);
|
||||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
|
initialize(emojiSelectionListener, variationSelectorListener, allowVariations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmojiPageView(@NonNull Context context,
|
||||||
|
@NonNull EmojiEventListener emojiSelectionListener,
|
||||||
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
|
boolean allowVariations,
|
||||||
|
@NonNull LinearLayoutManager layoutManager,
|
||||||
|
@LayoutRes int displayEmojiLayoutResId,
|
||||||
|
@LayoutRes int displayEmoticonLayoutResId)
|
||||||
|
{
|
||||||
|
super(context);
|
||||||
|
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, layoutManager, displayEmojiLayoutResId, displayEmoticonLayoutResId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
|
||||||
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
|
boolean allowVariations)
|
||||||
|
{
|
||||||
|
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, new GridLayoutManager(getContext(), 8), R.layout.emoji_display_item_grid, R.layout.emoji_text_display_item_grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
|
||||||
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
|
boolean allowVariations,
|
||||||
|
@NonNull LinearLayoutManager layoutManager,
|
||||||
|
@LayoutRes int displayEmojiLayoutResId,
|
||||||
|
@LayoutRes int displayEmoticonLayoutResId)
|
||||||
|
{
|
||||||
this.variationSelectorListener = variationSelectorListener;
|
this.variationSelectorListener = variationSelectorListener;
|
||||||
|
|
||||||
recyclerView = view.findViewById(R.id.emoji);
|
this.layoutManager = layoutManager;
|
||||||
layoutManager = new GridLayoutManager(context, 8);
|
this.scrollDisabler = new ScrollDisabler();
|
||||||
scrollDisabler = new ScrollDisabler();
|
this.popup = new EmojiVariationSelectorPopup(getContext(), emojiSelectionListener);
|
||||||
popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
|
this.adapterFactory = () -> new EmojiPageViewGridAdapter(popup,
|
||||||
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
|
emojiSelectionListener,
|
||||||
popup,
|
this,
|
||||||
emojiSelectionListener,
|
allowVariations,
|
||||||
this);
|
displayEmojiLayoutResId,
|
||||||
|
displayEmoticonLayoutResId);
|
||||||
|
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
if (this.layoutManager instanceof GridLayoutManager) {
|
||||||
recyclerView.setAdapter(adapter);
|
GridLayoutManager gridLayout = (GridLayoutManager) this.layoutManager;
|
||||||
|
gridLayout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||||
|
@Override
|
||||||
|
public int getSpanSize(int position) {
|
||||||
|
if (getAdapter() != null) {
|
||||||
|
Optional<MappingModel<?>> model = getAdapter().getModel(position);
|
||||||
|
if (model.isPresent() && (model.get() instanceof EmojiHeader || model.get() instanceof EmojiNoResultsModel)) {
|
||||||
|
return gridLayout.getSpanCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayoutManager(layoutManager);
|
||||||
|
|
||||||
|
Drawable drawable = DrawableUtil.tint(ContextUtil.requireDrawable(getContext(), R.drawable.triangle_bottom_right_corner), ContextCompat.getColor(getContext(), R.color.signal_button_secondary_text_disabled));
|
||||||
|
addItemDecoration(new EmojiItemDecoration(allowVariations, drawable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void presentForEmojiKeyboard() {
|
||||||
|
setPadding(getPaddingLeft(),
|
||||||
|
getPaddingTop(),
|
||||||
|
getPaddingRight(),
|
||||||
|
getPaddingBottom() + ViewUtil.dpToPx(56));
|
||||||
|
|
||||||
|
setClipToPadding(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSelected() {
|
public void onSelected() {
|
||||||
if (model.isDynamic() && adapter != null) {
|
if (getAdapter() != null) {
|
||||||
adapter.notifyDataSetChanged();
|
getAdapter().notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setModel(EmojiPageModel model) {
|
public void setList(@NonNull List<MappingModel<?>> list, @Nullable Runnable commitCallback) {
|
||||||
this.model = model;
|
EmojiPageViewGridAdapter adapter = adapterFactory.create();
|
||||||
adapter.setEmoji(model.getDisplayEmoji());
|
setAdapter(adapter);
|
||||||
|
adapter.submitList(list, commitCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -66,16 +142,21 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
|
if (layoutManager instanceof GridLayoutManager) {
|
||||||
layoutManager.setSpanCount(Math.max(w / idealWidth, 1));
|
int viewWidth = w - getPaddingStart() - getPaddingEnd();
|
||||||
|
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
|
||||||
|
int spanCount = Math.max(viewWidth / idealWidth, 1);
|
||||||
|
|
||||||
|
((GridLayoutManager) layoutManager).setSpanCount(spanCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onVariationSelectorStateChanged(boolean open) {
|
public void onVariationSelectorStateChanged(boolean open) {
|
||||||
if (open) {
|
if (open) {
|
||||||
recyclerView.addOnItemTouchListener(scrollDisabler);
|
addOnItemTouchListener(scrollDisabler);
|
||||||
} else {
|
} else {
|
||||||
post(() -> recyclerView.removeOnItemTouchListener(scrollDisabler));
|
post(() -> removeOnItemTouchListener(scrollDisabler));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variationSelectorListener != null) {
|
if (variationSelectorListener != null) {
|
||||||
|
@ -83,6 +164,32 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
|
||||||
|
setNestedScrollingEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void smoothScrollToPositionTop(int position) {
|
||||||
|
int currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition();
|
||||||
|
boolean shortTrip = Math.abs(currentPosition - position) < 475;
|
||||||
|
|
||||||
|
if (shortTrip) {
|
||||||
|
RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) {
|
||||||
|
@Override
|
||||||
|
protected int getVerticalSnapPreference() {
|
||||||
|
return LinearSmoothScroller.SNAP_TO_START;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
smoothScroller.setTargetPosition(position);
|
||||||
|
layoutManager.startSmoothScroll(smoothScroller);
|
||||||
|
} else {
|
||||||
|
layoutManager.scrollToPositionWithOffset(position, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable EmojiPageViewGridAdapter getAdapter() {
|
||||||
|
return (EmojiPageViewGridAdapter) super.getAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
|
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
|
||||||
@Override
|
@Override
|
||||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
|
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
|
||||||
|
@ -95,4 +202,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||||
@Override
|
@Override
|
||||||
public void onRequestDisallowInterceptTouchEvent(boolean b) { }
|
public void onRequestDisallowInterceptTouchEvent(boolean b) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private interface AdapterFactory {
|
||||||
|
EmojiPageViewGridAdapter create();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,94 +1,40 @@
|
||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.PopupWindow;
|
import android.widget.PopupWindow;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.LayoutRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory;
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWindow.OnDismissListener {
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageViewGridAdapter.EmojiViewHolder> implements PopupWindow.OnDismissListener {
|
private final VariationSelectorListener variationSelectorListener;
|
||||||
|
|
||||||
private final List<Emoji> emojiList;
|
public EmojiPageViewGridAdapter(@NonNull EmojiVariationSelectorPopup popup,
|
||||||
private final EmojiProvider emojiProvider;
|
|
||||||
private final EmojiVariationSelectorPopup popup;
|
|
||||||
private final VariationSelectorListener variationSelectorListener;
|
|
||||||
private final EmojiEventListener emojiEventListener;
|
|
||||||
|
|
||||||
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
|
|
||||||
@NonNull EmojiVariationSelectorPopup popup,
|
|
||||||
@NonNull EmojiEventListener emojiEventListener,
|
@NonNull EmojiEventListener emojiEventListener,
|
||||||
@NonNull VariationSelectorListener variationSelectorListener)
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
|
boolean allowVariations,
|
||||||
|
@LayoutRes int displayEmojiLayoutResId,
|
||||||
|
@LayoutRes int displayEmoticonLayoutResId)
|
||||||
{
|
{
|
||||||
this.emojiList = new ArrayList<>();
|
|
||||||
this.emojiProvider = emojiProvider;
|
|
||||||
this.popup = popup;
|
|
||||||
this.emojiEventListener = emojiEventListener;
|
|
||||||
this.variationSelectorListener = variationSelectorListener;
|
this.variationSelectorListener = variationSelectorListener;
|
||||||
|
|
||||||
popup.setOnDismissListener(this);
|
popup.setOnDismissListener(this);
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
registerFactory(EmojiHeader.class, new LayoutFactory<>(EmojiHeaderViewHolder::new, R.layout.emoji_grid_header));
|
||||||
@Override
|
registerFactory(EmojiModel.class, new LayoutFactory<>(v -> new EmojiViewHolder(v, emojiEventListener, variationSelectorListener, popup, allowVariations), displayEmojiLayoutResId));
|
||||||
public EmojiViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
registerFactory(EmojiTextModel.class, new LayoutFactory<>(v -> new EmojiTextViewHolder(v, emojiEventListener), displayEmoticonLayoutResId));
|
||||||
return new EmojiViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.emoji_display_item, viewGroup, false));
|
registerFactory(EmojiNoResultsModel.class, new LayoutFactory<>(MappingViewHolder.SimpleViewHolder::new, R.layout.emoji_grid_no_results));
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull EmojiViewHolder viewHolder, int i) {
|
|
||||||
Emoji emoji = emojiList.get(i);
|
|
||||||
|
|
||||||
Drawable drawable = emojiProvider.getEmojiDrawable(emoji.getValue());
|
|
||||||
|
|
||||||
if (drawable != null) {
|
|
||||||
viewHolder.textView.setVisibility(View.GONE);
|
|
||||||
viewHolder.imageView.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
viewHolder.imageView.setImageDrawable(drawable);
|
|
||||||
} else {
|
|
||||||
viewHolder.textView.setVisibility(View.VISIBLE);
|
|
||||||
viewHolder.imageView.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
viewHolder.textView.setEmoji(emoji.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
viewHolder.itemView.setOnClickListener(v -> {
|
|
||||||
emojiEventListener.onEmojiSelected(emoji.getValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emoji.getVariations().size() > 1) {
|
|
||||||
viewHolder.itemView.setOnLongClickListener(v -> {
|
|
||||||
popup.dismiss();
|
|
||||||
popup.setVariations(emoji.getVariations());
|
|
||||||
popup.showAsDropDown(viewHolder.itemView, 0, -(2 * viewHolder.itemView.getHeight()));
|
|
||||||
variationSelectorListener.onVariationSelectorStateChanged(true);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
viewHolder.hintCorner.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
viewHolder.itemView.setOnLongClickListener(null);
|
|
||||||
viewHolder.hintCorner.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return emojiList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEmoji(@NonNull List<Emoji> emojiList) {
|
|
||||||
this.emojiList.clear();
|
|
||||||
this.emojiList.addAll(emojiList);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -96,18 +42,196 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
|
||||||
variationSelectorListener.onVariationSelectorStateChanged(false);
|
variationSelectorListener.onVariationSelectorStateChanged(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class EmojiViewHolder extends RecyclerView.ViewHolder {
|
public static class EmojiHeader implements MappingModel<EmojiHeader>, HasKey {
|
||||||
|
|
||||||
private final ImageView imageView;
|
private final String key;
|
||||||
private final AsciiEmojiView textView;
|
private final int title;
|
||||||
private final ImageView hintCorner;
|
|
||||||
|
|
||||||
public EmojiViewHolder(@NonNull View itemView) {
|
public EmojiHeader(@NonNull String key, int title) {
|
||||||
super(itemView);
|
this.key = key;
|
||||||
this.imageView = itemView.findViewById(R.id.emoji_image);
|
this.title = title;
|
||||||
this.textView = itemView.findViewById(R.id.emoji_text);
|
|
||||||
this.hintCorner = itemView.findViewById(R.id.emoji_variation_hint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull EmojiHeader newItem) {
|
||||||
|
return title == newItem.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull EmojiHeader newItem) {
|
||||||
|
return areItemsTheSame(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class EmojiHeaderViewHolder extends MappingViewHolder<EmojiHeader> {
|
||||||
|
|
||||||
|
private final TextView title;
|
||||||
|
|
||||||
|
public EmojiHeaderViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
title = findViewById(R.id.emoji_grid_header_title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bind(@NonNull EmojiHeader model) {
|
||||||
|
title.setText(model.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmojiModel implements MappingModel<EmojiModel>, HasKey {
|
||||||
|
|
||||||
|
private final String key;
|
||||||
|
private final Emoji emoji;
|
||||||
|
|
||||||
|
public EmojiModel(@NonNull String key, @NonNull Emoji emoji) {
|
||||||
|
this.key = key;
|
||||||
|
this.emoji = emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Emoji getEmoji() {
|
||||||
|
return emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull EmojiModel newItem) {
|
||||||
|
return newItem.emoji.getValue().equals(emoji.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull EmojiModel newItem) {
|
||||||
|
return areItemsTheSame(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class EmojiViewHolder extends MappingViewHolder<EmojiModel> {
|
||||||
|
|
||||||
|
private final EmojiVariationSelectorPopup popup;
|
||||||
|
private final VariationSelectorListener variationSelectorListener;
|
||||||
|
private final EmojiEventListener emojiEventListener;
|
||||||
|
private final boolean allowVariations;
|
||||||
|
|
||||||
|
private final ImageView imageView;
|
||||||
|
|
||||||
|
public EmojiViewHolder(@NonNull View itemView,
|
||||||
|
@NonNull EmojiEventListener emojiEventListener,
|
||||||
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
|
@NonNull EmojiVariationSelectorPopup popup,
|
||||||
|
boolean allowVariations)
|
||||||
|
{
|
||||||
|
super(itemView);
|
||||||
|
|
||||||
|
this.popup = popup;
|
||||||
|
this.variationSelectorListener = variationSelectorListener;
|
||||||
|
this.emojiEventListener = emojiEventListener;
|
||||||
|
this.allowVariations = allowVariations;
|
||||||
|
|
||||||
|
this.imageView = itemView.findViewById(R.id.emoji_image);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bind(@NonNull EmojiModel model) {
|
||||||
|
final Drawable drawable = EmojiProvider.getEmojiDrawable(imageView.getContext(), model.emoji.getValue());
|
||||||
|
|
||||||
|
if (drawable != null) {
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
imageView.setImageDrawable(drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.setOnClickListener(v -> {
|
||||||
|
emojiEventListener.onEmojiSelected(model.emoji.getValue());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allowVariations && model.emoji.hasMultipleVariations()) {
|
||||||
|
itemView.setOnLongClickListener(v -> {
|
||||||
|
popup.dismiss();
|
||||||
|
popup.setVariations(model.emoji.getVariations());
|
||||||
|
popup.showAsDropDown(itemView, 0, -(2 * itemView.getHeight()));
|
||||||
|
variationSelectorListener.onVariationSelectorStateChanged(true);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
itemView.setOnLongClickListener(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmojiTextModel implements MappingModel<EmojiTextModel>, HasKey {
|
||||||
|
private final String key;
|
||||||
|
private final Emoji emoji;
|
||||||
|
|
||||||
|
public EmojiTextModel(@NonNull String key, @NonNull Emoji emoji) {
|
||||||
|
this.key = key;
|
||||||
|
this.emoji = emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Emoji getEmoji() {
|
||||||
|
return emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull EmojiTextModel newItem) {
|
||||||
|
return newItem.emoji.getValue().equals(emoji.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull EmojiTextModel newItem) {
|
||||||
|
return areItemsTheSame(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class EmojiTextViewHolder extends MappingViewHolder<EmojiTextModel> {
|
||||||
|
|
||||||
|
private final EmojiEventListener emojiEventListener;
|
||||||
|
private final AsciiEmojiView textView;
|
||||||
|
|
||||||
|
public EmojiTextViewHolder(@NonNull View itemView,
|
||||||
|
@NonNull EmojiEventListener emojiEventListener)
|
||||||
|
{
|
||||||
|
super(itemView);
|
||||||
|
|
||||||
|
this.emojiEventListener = emojiEventListener;
|
||||||
|
this.textView = itemView.findViewById(R.id.emoji_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bind(@NonNull EmojiTextModel model) {
|
||||||
|
textView.setEmoji(model.emoji.getValue());
|
||||||
|
|
||||||
|
itemView.setOnClickListener(v -> {
|
||||||
|
emojiEventListener.onEmojiSelected(model.emoji.getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmojiNoResultsModel implements MappingModel<EmojiNoResultsModel> {
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull EmojiNoResultsModel newItem) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull EmojiNoResultsModel newItem) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface HasKey {
|
||||||
|
@NonNull String getKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface VariationSelectorListener {
|
public interface VariationSelectorListener {
|
||||||
|
|