Compare commits
308 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 |
|
@ -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
|
||||
*.sh
|
||||
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".
|
||||
5. Default config options should be good enough.
|
||||
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
|
||||
-----------------
|
||||
|
|
|
@ -10,7 +10,7 @@ Add the [F-Droid repo](https://fdroid.getsession.org/)
|
|||
|
||||
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?
|
||||
|
||||
|
|
335
app/build.gradle
335
app/build.gradle
|
@ -1,3 +1,4 @@
|
|||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
|
@ -13,12 +14,16 @@ buildscript {
|
|||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'kotlin-kapt'
|
||||
id 'com.google.dagger.hilt.android'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
|
||||
|
@ -26,142 +31,8 @@ configurations.all {
|
|||
exclude module: "commons-logging"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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.cardview:cardview:1.0.0'
|
||||
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
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-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
|
||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
implementation ("com.google.firebase:firebase-messaging:18.0.0") {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||
implementation 'commons-net:commons-net:3.7.2'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation "com.google.dagger:hilt-android:$daggerVersion"
|
||||
kapt "com.google.dagger:hilt-compiler:$daggerVersion"
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||
}
|
||||
implementation 'com.annimon:stream:1.1.8'
|
||||
implementation 'com.takisoft.fix:colorpicker:1.0.1'
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
implementation 'androidx.sqlite:sqlite-ktx:2.2.0'
|
||||
implementation 'net.zetetic:sqlcipher-android:4.5.3@aar'
|
||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
exclude group: 'org.freemarker'
|
||||
}
|
||||
implementation project(":libsignal")
|
||||
implementation project(":libsession")
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
|
||||
implementation project(":liblazysodium")
|
||||
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation 'app.cash.copper:copper-flow:1.0.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant:$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.github.tbruyelle:rxpermissions:0.10.2"
|
||||
implementation "com.github.ybq:Android-SpinKit:1.4.0"
|
||||
implementation "com.opencsv:opencsv:4.6"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation "org.mockito:mockito-inline:4.0.0"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
testImplementation "androidx.test:core:$testCoreVersion"
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||
// Core library
|
||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||
|
||||
// AndroidJUnitRunner and JUnit Rules
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
|
||||
// Assertions
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.ext:truth:1.4.0'
|
||||
androidTestImplementation 'com.google.truth:truth:1.1.3'
|
||||
|
||||
// Espresso dependencies
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 335
|
||||
def canonicalVersionName = "1.16.7"
|
||||
def canonicalVersionCode = 360
|
||||
def canonicalVersionName = "1.17.5"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -203,6 +74,13 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.4.7'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
@ -244,6 +122,7 @@ android {
|
|||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
isDefault true
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
|
@ -251,15 +130,32 @@ android {
|
|||
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', '\"\"'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -289,6 +185,166 @@ android {
|
|||
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 {
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.46.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.cardview:cardview:1.0.0'
|
||||
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
|
||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||
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-analytics'
|
||||
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-ui:2.9.1'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||
implementation 'commons-net:commons-net:3.7.2'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation "com.google.dagger:hilt-android:$daggerVersion"
|
||||
kapt "com.google.dagger:hilt-compiler:$daggerVersion"
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||
}
|
||||
implementation 'com.annimon:stream:1.1.8'
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
implementation 'androidx.sqlite:sqlite-ktx:2.3.1'
|
||||
implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'
|
||||
implementation project(":libsignal")
|
||||
implementation project(":libsession")
|
||||
implementation project(":libsession-util")
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
|
||||
implementation project(":liblazysodium")
|
||||
implementation "net.java.dev.jna:jna:5.12.1@aar"
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation 'app.cash.copper:copper-flow:1.0.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
||||
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
|
||||
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
|
||||
implementation "com.github.ybq:Android-SpinKit:1.4.0"
|
||||
implementation "com.opencsv:opencsv:4.6"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation "org.mockito:mockito-inline:4.10.0"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||
androidTestImplementation "org.mockito:mockito-android:4.10.0"
|
||||
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||
testImplementation "androidx.test:core:$testCoreVersion"
|
||||
testImplementation "androidx.arch.core:core-testing:2.2.0"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||
// Core library
|
||||
androidTestImplementation "androidx.test:core:$testCoreVersion"
|
||||
|
||||
androidTestImplementation('com.adevinta.android:barista:4.2.0') {
|
||||
exclude group: 'org.jetbrains.kotlin'
|
||||
}
|
||||
|
||||
// AndroidJUnitRunner and JUnit Rules
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
|
||||
// Assertions
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.ext:truth:1.5.0'
|
||||
androidTestImplementation 'com.google.truth:truth:1.1.3'
|
||||
|
||||
// Espresso dependencies
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
|
||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.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"
|
||||
|
||||
implementation 'androidx.compose.foundation:foundation-layout:1.5.2'
|
||||
implementation 'androidx.compose.material:material:1.5.2'
|
||||
}
|
||||
|
||||
static def getLastCommitTimestamp() {
|
||||
|
@ -309,3 +365,8 @@ def autoResConfig() {
|
|||
.collect { matcher -> matcher.group(1) }
|
||||
.sort()
|
||||
}
|
||||
|
||||
// Allow references to generated code
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package network.loki.messenger
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Instrumentation
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
|
@ -21,6 +22,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.adevinta.android.barista.interaction.PermissionGranter
|
||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers.allOf
|
||||
|
@ -85,6 +87,8 @@ class HomeActivityTests {
|
|||
}
|
||||
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
|
||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
||||
// allow notification permission
|
||||
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
private fun goToMyChat() {
|
||||
|
@ -100,6 +104,7 @@ class HomeActivityTests {
|
|||
copied = clipboardManager.primaryClip!!.getItemAt(0).text.toString()
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
|
@ -153,6 +158,7 @@ class HomeActivityTests {
|
|||
|
||||
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()))
|
||||
}
|
||||
|
||||
|
|
|
@ -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,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>
|
|
@ -29,12 +29,18 @@
|
|||
android:name="android.hardware.touchscreen"
|
||||
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_CONNECT" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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="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.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
@ -231,10 +237,6 @@
|
|||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight">
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.DatabaseUpgradeActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
|
@ -306,20 +308,16 @@
|
|||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||
</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"
|
||||
android:foregroundServiceType="microphone"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.service.KeyCachingService"
|
||||
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
|
||||
android:name="org.thoughtcrime.securesms.service.DirectShareService"
|
||||
android:exported="true"
|
||||
|
@ -407,12 +405,6 @@
|
|||
<action android:name="network.loki.securesms.RESTART" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
|
@ -446,17 +438,9 @@
|
|||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name="org.thoughtcrime.securesms.jobmanager.KeepAliveService"
|
||||
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
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
android:required="false" />
|
||||
|
|
|
@ -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.snode.SnodeModule;
|
||||
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.SSKEnvironment;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
@ -55,34 +57,29 @@ import org.signal.aesgcmprovider.AesGcmProvider;
|
|||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||
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.DatabaseModule;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
||||
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
|
||||
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.OptimizedMessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.PushRegistry;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
|
||||
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
|
||||
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
|
||||
|
@ -113,6 +110,8 @@ import dagger.hilt.android.HiltAndroidApp;
|
|||
import kotlin.Unit;
|
||||
import kotlinx.coroutines.Job;
|
||||
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.
|
||||
|
@ -123,7 +122,7 @@ import network.loki.messenger.BuildConfig;
|
|||
* @author Moxie Marlinspike
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
public class ApplicationContext extends Application implements DefaultLifecycleObserver {
|
||||
public class ApplicationContext extends Application implements DefaultLifecycleObserver, ConfigFactoryUpdateListener {
|
||||
|
||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
|
@ -132,7 +131,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
private ExpiringMessageManager expiringMessageManager;
|
||||
private TypingStatusRepository typingStatusRepository;
|
||||
private TypingStatusSender typingStatusSender;
|
||||
private JobManager jobManager;
|
||||
private ReadReceiptManager readReceiptManager;
|
||||
private ProfileManager profileManager;
|
||||
public MessageNotifier messageNotifier = null;
|
||||
|
@ -145,10 +143,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
private PersistentLogger persistentLogger;
|
||||
|
||||
@Inject LokiAPIDatabase lokiAPIDatabase;
|
||||
@Inject Storage storage;
|
||||
@Inject public Storage storage;
|
||||
@Inject Device device;
|
||||
@Inject MessageDataProvider messageDataProvider;
|
||||
@Inject JobDatabase jobDatabase;
|
||||
@Inject TextSecurePreferences textSecurePreferences;
|
||||
@Inject PushRegistry pushRegistry;
|
||||
@Inject ConfigFactory configFactory;
|
||||
CallMessageProcessor callMessageProcessor;
|
||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||
|
||||
|
@ -167,7 +167,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
}
|
||||
|
||||
public TextSecurePreferences getPrefs() {
|
||||
return textSecurePreferences;
|
||||
return EntryPoints.get(getApplicationContext(), AppComponent.class).getPrefs();
|
||||
}
|
||||
|
||||
public DatabaseComponent getDatabaseComponent() {
|
||||
|
@ -196,15 +196,30 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
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
|
||||
public void onCreate() {
|
||||
TextSecurePreferences.setPushSuffix(BuildConfig.PUSH_KEY_SUFFIX);
|
||||
|
||||
DatabaseModule.init(this);
|
||||
MessagingModuleConfiguration.configure(this);
|
||||
super.onCreate();
|
||||
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
|
||||
messagingModuleConfiguration = new MessagingModuleConfiguration(
|
||||
this,
|
||||
storage,
|
||||
device,
|
||||
messageDataProvider,
|
||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
|
||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
||||
configFactory
|
||||
);
|
||||
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
||||
Log.i(TAG, "onCreate()");
|
||||
startKovenant();
|
||||
|
@ -218,10 +233,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
broadcaster = new Broadcaster(this);
|
||||
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey != null) {
|
||||
registerForFCMIfNeeded(false);
|
||||
}
|
||||
initializeExpiringMessageManager();
|
||||
initializeTypingStatusRepository();
|
||||
initializeTypingStatusSender();
|
||||
|
@ -229,7 +240,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
initializeProfileManager();
|
||||
initializePeriodicTasks();
|
||||
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
|
||||
initializeJobManager();
|
||||
initializeWebRtc();
|
||||
initializeBlobProvider();
|
||||
resubmitProfilePictureIfNeeded();
|
||||
|
@ -286,10 +296,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
LocaleParser.Companion.configure(new LocaleParseHelper());
|
||||
}
|
||||
|
||||
public JobManager getJobManager() {
|
||||
return jobManager;
|
||||
}
|
||||
|
||||
public ExpiringMessageManager getExpiringMessageManager() {
|
||||
return expiringMessageManager;
|
||||
}
|
||||
|
@ -352,16 +358,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
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() {
|
||||
this.expiringMessageManager = new ExpiringMessageManager(this);
|
||||
}
|
||||
|
@ -375,7 +371,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
}
|
||||
|
||||
private void initializeProfileManager() {
|
||||
this.profileManager = new ProfileManager();
|
||||
this.profileManager = new ProfileManager(this, configFactory);
|
||||
}
|
||||
|
||||
private void initializeTypingStatusSender() {
|
||||
|
@ -384,10 +380,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
|
||||
private void initializePeriodicTasks() {
|
||||
BackgroundPollWorker.schedulePeriodic(this);
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeWebRtc() {
|
||||
|
@ -438,33 +430,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
if (TextSecurePreferences.isUsingFCM(this)) {
|
||||
LokiPushNotificationManager.register(token, userPublicKey, this, force);
|
||||
} else {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
}
|
||||
});
|
||||
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpPollingIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return;
|
||||
|
@ -472,7 +437,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
poller.setUserPublicKey(userPublicKey);
|
||||
return;
|
||||
}
|
||||
poller = new Poller();
|
||||
poller = new Poller(configFactory, new Timer());
|
||||
}
|
||||
|
||||
public void startPollingIfNeeded() {
|
||||
|
@ -515,6 +480,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
});
|
||||
} catch (Exception exception) {
|
||||
// Do nothing
|
||||
Log.e("Loki-Avatar", "Uploading avatar failed", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -534,24 +500,21 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||
}
|
||||
|
||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||
String token = TextSecurePreferences.getFCMToken(this);
|
||||
if (token != null && !token.isEmpty()) {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
}
|
||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
|
||||
firebaseInstanceIdJob.cancel(null);
|
||||
}
|
||||
String displayName = TextSecurePreferences.getProfileName(this);
|
||||
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
||||
boolean isUsingFCM = TextSecurePreferences.isPushEnabled(this);
|
||||
TextSecurePreferences.clearAll(this);
|
||||
if (isMigratingToV2KeyPair) {
|
||||
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
||||
TextSecurePreferences.setPushEnabled(this, isUsingFCM);
|
||||
TextSecurePreferences.setProfileName(this, displayName);
|
||||
}
|
||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
|
||||
Log.d("Loki", "Failed to delete database.");
|
||||
}
|
||||
configFactory.keyPairChanged();
|
||||
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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;
|
||||
|
@ -18,6 +19,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
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;
|
||||
|
@ -92,6 +94,11 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
|||
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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -76,6 +76,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
|
@ -318,9 +319,9 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
|||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint({"InlinedApi", "StaticFieldLeak"})
|
||||
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)
|
||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||
|
@ -362,7 +363,8 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
|||
}.execute();
|
||||
})
|
||||
.execute();
|
||||
}, mediaRecords.size());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void sendMediaSavedNotificationIfNeeded() {
|
||||
|
@ -374,41 +376,26 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
|||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
||||
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());
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setTitle(confirmTitle);
|
||||
builder.setMessage(confirmMessage);
|
||||
builder.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.delete, (dialogInterface, i) -> {
|
||||
new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(getContext(),
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_title,
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_message)
|
||||
{
|
||||
@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());
|
||||
}
|
||||
DeleteMediaDialog.show(
|
||||
requireContext(),
|
||||
recordCount,
|
||||
() -> new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(
|
||||
requireContext(),
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_title,
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_message) {
|
||||
@Override
|
||||
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
|
||||
if (records == null || records.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()]));
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
for (MediaDatabase.MediaRecord record : records) {
|
||||
AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()])));
|
||||
}
|
||||
|
||||
private void handleSelectAllMedia() {
|
||||
|
|
|
@ -47,7 +47,6 @@ import android.widget.Toast;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
@ -85,6 +84,7 @@ import java.io.IOException;
|
|||
import java.util.Locale;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
|
@ -146,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) {
|
||||
Intent previewIntent = null;
|
||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
|
@ -416,7 +420,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
if (mediaItem == null) return;
|
||||
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
SaveAttachmentTask.showWarningDialog(this, 1, () -> {
|
||||
Permissions.with(this)
|
||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||
|
@ -433,6 +437,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||
}
|
||||
})
|
||||
.execute();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -449,29 +454,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||
return;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
|
||||
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
|
||||
builder.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.delete, (dialogInterface, which) -> {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
if (mediaItem.attachment == null) {
|
||||
return null;
|
||||
}
|
||||
AttachmentUtil.deleteAttachment(MediaPreviewActivity.this.getApplicationContext(),
|
||||
mediaItem.attachment);
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
DeleteMediaPreviewDialog.show(this, () -> {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
DatabaseAttachment attachment = mediaItem.attachment;
|
||||
if (attachment != null) {
|
||||
AttachmentUtil.deleteAttachment(getApplicationContext(), attachment);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
|
||||
finish();
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -531,18 +527,21 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
|
||||
if (data != null) {
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
|
||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||
|
||||
int item = restartItem >= 0 ? restartItem : data.second;
|
||||
mediaPager.setCurrentItem(item);
|
||||
if (restartItem >= 0 || data.second >= 0) {
|
||||
int item = restartItem >= 0 ? restartItem : data.second;
|
||||
mediaPager.setCurrentItem(item);
|
||||
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
if (item == 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 })
|
||||
}
|
|
@ -9,13 +9,14 @@ import android.os.Bundle;
|
|||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||
import org.thoughtcrime.securesms.onboarding.LandingActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
|
@ -168,7 +169,13 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
};
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -7,6 +7,10 @@ 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) {
|
||||
|
||||
|
@ -31,22 +35,26 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
|
|||
val projection = arrayOf(
|
||||
MediaStore.Images.Media.DATA
|
||||
)
|
||||
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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,28 +64,32 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
|
|||
MediaStore.Images.Media.DISPLAY_NAME,
|
||||
MediaStore.Images.Media.RELATIVE_PATH
|
||||
)
|
||||
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()
|
||||
|
||||
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,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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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.zetetic.database.sqlcipher.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.zetetic.database.sqlcipher.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 ->
|
||||
Log.d("Loki", "Consuming view model state $state")
|
||||
when (state) {
|
||||
CALL_RINGING -> {
|
||||
if (wantsToAnswer) {
|
||||
answerCall()
|
||||
wantsToAnswer = false
|
||||
}
|
||||
}
|
||||
CALL_OUTGOING -> {
|
||||
}
|
||||
CALL_CONNECTED -> {
|
||||
CALL_RINGING -> if (wantsToAnswer) {
|
||||
answerCall()
|
||||
wantsToAnswer = false
|
||||
}
|
||||
CALL_CONNECTED -> wantsToAnswer = false
|
||||
else -> {}
|
||||
}
|
||||
updateControls(state)
|
||||
}
|
||||
|
|
|
@ -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,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.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
|
@ -18,13 +19,14 @@ import org.session.libsession.utilities.Address
|
|||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class ProfilePictureView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : RelativeLayout(context, attrs) {
|
||||
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
|
||||
lateinit var glide: GlideRequests
|
||||
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
||||
private val glide: GlideRequests = GlideApp.with(this)
|
||||
var publicKey: String? = null
|
||||
var displayName: String? = null
|
||||
var additionalPublicKey: String? = null
|
||||
|
@ -32,13 +34,18 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||
var isLarge = false
|
||||
|
||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
||||
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||
private val unknownOpenGroupDrawable = ResourceContactPhoto(R.drawable.ic_notification)
|
||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||
.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
|
||||
|
||||
constructor(context: Context, sender: Recipient): this(context) {
|
||||
update(sender)
|
||||
}
|
||||
|
||||
// region Updating
|
||||
fun update(recipient: Recipient) {
|
||||
fun getUserDisplayName(publicKey: String): String {
|
||||
|
@ -52,14 +59,21 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||
.sorted()
|
||||
.take(2)
|
||||
.toMutableList()
|
||||
val pk = members.getOrNull(0)?.serialize() ?: ""
|
||||
publicKey = pk
|
||||
displayName = getUserDisplayName(pk)
|
||||
val apk = members.getOrNull(1)?.serialize() ?: ""
|
||||
additionalPublicKey = apk
|
||||
additionalDisplayName = getUserDisplayName(apk)
|
||||
if (members.size <= 1) {
|
||||
publicKey = ""
|
||||
displayName = ""
|
||||
additionalPublicKey = ""
|
||||
additionalDisplayName = ""
|
||||
} 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.getDecodedOpenGroupInbox(recipient.address.serialize())
|
||||
val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
|
||||
this.publicKey = publicKey
|
||||
displayName = getUserDisplayName(publicKey)
|
||||
additionalPublicKey = null
|
||||
|
@ -73,7 +87,6 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
fun update() {
|
||||
if (!this::glide.isInitialized) return
|
||||
val publicKey = publicKey ?: return
|
||||
val additionalPublicKey = additionalPublicKey
|
||||
if (additionalPublicKey != null) {
|
||||
|
@ -108,30 +121,36 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||
val signalProfilePicture = recipient.contactPhoto
|
||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||
|
||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
|
||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||
glide.clear(imageView)
|
||||
glide.load(signalProfilePicture)
|
||||
.placeholder(unknownRecipientDrawable)
|
||||
.centerCrop()
|
||||
.error(unknownRecipientDrawable)
|
||||
.error(glide.load(placeholder))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.into(imageView)
|
||||
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
|
||||
glide.clear(imageView)
|
||||
imageView.setImageDrawable(unknownOpenGroupDrawable)
|
||||
glide.load(unknownOpenGroupDrawable)
|
||||
.centerCrop()
|
||||
.circleCrop()
|
||||
.into(imageView)
|
||||
} else {
|
||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
|
||||
glide.clear(imageView)
|
||||
glide.load(placeholder)
|
||||
.placeholder(unknownRecipientDrawable)
|
||||
.centerCrop()
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
||||
}
|
||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
||||
} else {
|
||||
imageView.setImageDrawable(null)
|
||||
glide.load(unknownRecipientDrawable)
|
||||
.centerCrop()
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
|
||||
import org.session.libsession.utilities.ListenableFutureTask;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class EmojiPageBitmap {
|
||||
|
||||
private static final String TAG = EmojiPageBitmap.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final EmojiPageModel model;
|
||||
private final float decodeScale;
|
||||
|
||||
private SoftReference<Bitmap> bitmapReference;
|
||||
private ListenableFutureTask<Bitmap> task;
|
||||
|
||||
public EmojiPageBitmap(@NonNull Context context, @NonNull EmojiPageModel model, float decodeScale) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.model = model;
|
||||
this.decodeScale = decodeScale;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public ListenableFutureTask<Bitmap> get() {
|
||||
Util.assertMainThread();
|
||||
|
||||
if (bitmapReference != null && bitmapReference.get() != null) {
|
||||
return new ListenableFutureTask<>(bitmapReference.get());
|
||||
} else if (task != null) {
|
||||
return task;
|
||||
} else {
|
||||
Callable<Bitmap> callable = () -> {
|
||||
try {
|
||||
Log.i(TAG, "loading page " + model.getSpriteUri().toString());
|
||||
return loadPage();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
task = new ListenableFutureTask<>(callable);
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override protected Void doInBackground(Void... params) {
|
||||
task.run();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override protected void onPostExecute(Void aVoid) {
|
||||
task = null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private Bitmap loadPage() throws IOException {
|
||||
if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get();
|
||||
|
||||
|
||||
float scale = decodeScale;
|
||||
AssetManager assetManager = context.getAssets();
|
||||
InputStream assetStream = assetManager.open(model.getSpriteUri().toString());
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
|
||||
if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) {
|
||||
Log.i(TAG, "Low memory detected. Changing sample size.");
|
||||
options.inSampleSize = 2;
|
||||
scale = decodeScale * 2;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch(model.getSpriteUri().toString());
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(assetStream, null, options);
|
||||
stopwatch.split("decode");
|
||||
|
||||
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, (int)(bitmap.getWidth() * scale), (int)(bitmap.getHeight() * scale), true);
|
||||
stopwatch.split("scale");
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
bitmapReference = new SoftReference<>(scaledBitmap);
|
||||
Log.i(TAG, "onPageLoaded(" + model.getSpriteUri().toString() + ") originalByteCount: " + bitmap.getByteCount()
|
||||
+ " scaledByteCount: " + scaledBitmap.getByteCount()
|
||||
+ " scaledSize: " + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight());
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return model.getSpriteUri().toString();
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.recyclerview;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class SmoothScrollingLinearLayoutManager extends LinearLayoutManager {
|
||||
|
||||
public SmoothScrollingLinearLayoutManager(Context context, boolean reverseLayout) {
|
||||
super(context, LinearLayoutManager.VERTICAL, reverseLayout);
|
||||
}
|
||||
|
||||
public void smoothScrollToPosition(@NonNull Context context, int position, float millisecondsPerInch) {
|
||||
final LinearSmoothScroller scroller = new LinearSmoothScroller(context) {
|
||||
@Override
|
||||
protected int getVerticalSnapPreference() {
|
||||
return LinearSmoothScroller.SNAP_TO_END;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
||||
return millisecondsPerInch / displayMetrics.densityDpi;
|
||||
}
|
||||
};
|
||||
|
||||
scroller.setTargetPosition(position);
|
||||
startSmoothScroll(scroller);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -24,7 +24,7 @@ public final class ContactUtil {
|
|||
return SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message));
|
||||
}
|
||||
|
||||
public static @NonNull String getDisplayName(@Nullable Contact contact) {
|
||||
private static @NonNull String getDisplayName(@Nullable Contact contact) {
|
||||
if (contact == null) {
|
||||
return "";
|
||||
}
|
|
@ -7,6 +7,7 @@ import android.view.View
|
|||
import android.widget.LinearLayout
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewUserBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
|
||||
|
@ -47,15 +48,14 @@ class UserView : LinearLayout {
|
|||
|
||||
// region Updating
|
||||
fun bind(user: Recipient, glide: GlideRequests, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
|
||||
val isLocalUser = user.isLocalNumber
|
||||
fun getUserDisplayName(publicKey: String): String {
|
||||
if (isLocalUser) return context.getString(R.string.MessageRecord_you)
|
||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||
}
|
||||
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
|
||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
||||
val address = user.address.serialize()
|
||||
binding.profilePictureView.root.glide = glide
|
||||
binding.profilePictureView.root.update(user)
|
||||
binding.profilePictureView.update(user)
|
||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||
when (actionIndicator) {
|
||||
|
@ -87,7 +87,7 @@ class UserView : LinearLayout {
|
|||
}
|
||||
|
||||
fun unbind() {
|
||||
binding.profilePictureView.root.recycle()
|
||||
binding.profilePictureView.recycle()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.session.libsignal.messages.SharedContact;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import static org.session.libsession.utilities.Contact.*;
|
||||
|
||||
public class ContactModelMapper {
|
||||
|
||||
public static SharedContact.Builder localToRemoteBuilder(@NonNull Contact contact) {
|
||||
List<SharedContact.Phone> phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size());
|
||||
List<SharedContact.Email> emails = new ArrayList<>(contact.getEmails().size());
|
||||
List<SharedContact.PostalAddress> postalAddresses = new ArrayList<>(contact.getPostalAddresses().size());
|
||||
|
||||
for (Phone phone : contact.getPhoneNumbers()) {
|
||||
phoneNumbers.add(new SharedContact.Phone.Builder().setValue(phone.getNumber())
|
||||
.setType(localToRemoteType(phone.getType()))
|
||||
.setLabel(phone.getLabel())
|
||||
.build());
|
||||
}
|
||||
|
||||
for (Email email : contact.getEmails()) {
|
||||
emails.add(new SharedContact.Email.Builder().setValue(email.getEmail())
|
||||
.setType(localToRemoteType(email.getType()))
|
||||
.setLabel(email.getLabel())
|
||||
.build());
|
||||
}
|
||||
|
||||
for (PostalAddress postalAddress : contact.getPostalAddresses()) {
|
||||
postalAddresses.add(new SharedContact.PostalAddress.Builder().setType(localToRemoteType(postalAddress.getType()))
|
||||
.setLabel(postalAddress.getLabel())
|
||||
.setStreet(postalAddress.getStreet())
|
||||
.setPobox(postalAddress.getPoBox())
|
||||
.setNeighborhood(postalAddress.getNeighborhood())
|
||||
.setCity(postalAddress.getCity())
|
||||
.setRegion(postalAddress.getRegion())
|
||||
.setPostcode(postalAddress.getPostalCode())
|
||||
.setCountry(postalAddress.getCountry())
|
||||
.build());
|
||||
}
|
||||
|
||||
SharedContact.Name name = new SharedContact.Name.Builder().setDisplay(contact.getName().getDisplayName())
|
||||
.setGiven(contact.getName().getGivenName())
|
||||
.setFamily(contact.getName().getFamilyName())
|
||||
.setPrefix(contact.getName().getPrefix())
|
||||
.setSuffix(contact.getName().getSuffix())
|
||||
.setMiddle(contact.getName().getMiddleName())
|
||||
.build();
|
||||
|
||||
return new SharedContact.Builder().setName(name)
|
||||
.withOrganization(contact.getOrganization())
|
||||
.withPhones(phoneNumbers)
|
||||
.withEmails(emails)
|
||||
.withAddresses(postalAddresses);
|
||||
}
|
||||
|
||||
public static Contact remoteToLocal(@NonNull SharedContact sharedContact) {
|
||||
Name name = new Name(sharedContact.getName().getDisplay().orNull(),
|
||||
sharedContact.getName().getGiven().orNull(),
|
||||
sharedContact.getName().getFamily().orNull(),
|
||||
sharedContact.getName().getPrefix().orNull(),
|
||||
sharedContact.getName().getSuffix().orNull(),
|
||||
sharedContact.getName().getMiddle().orNull());
|
||||
|
||||
List<Phone> phoneNumbers = new LinkedList<>();
|
||||
if (sharedContact.getPhone().isPresent()) {
|
||||
for (SharedContact.Phone phone : sharedContact.getPhone().get()) {
|
||||
phoneNumbers.add(new Phone(phone.getValue(),
|
||||
remoteToLocalType(phone.getType()),
|
||||
phone.getLabel().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
List<Email> emails = new LinkedList<>();
|
||||
if (sharedContact.getEmail().isPresent()) {
|
||||
for (SharedContact.Email email : sharedContact.getEmail().get()) {
|
||||
emails.add(new Email(email.getValue(),
|
||||
remoteToLocalType(email.getType()),
|
||||
email.getLabel().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
List<PostalAddress> postalAddresses = new LinkedList<>();
|
||||
if (sharedContact.getAddress().isPresent()) {
|
||||
for (SharedContact.PostalAddress postalAddress : sharedContact.getAddress().get()) {
|
||||
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()),
|
||||
postalAddress.getLabel().orNull(),
|
||||
postalAddress.getStreet().orNull(),
|
||||
postalAddress.getPobox().orNull(),
|
||||
postalAddress.getNeighborhood().orNull(),
|
||||
postalAddress.getCity().orNull(),
|
||||
postalAddress.getRegion().orNull(),
|
||||
postalAddress.getPostcode().orNull(),
|
||||
postalAddress.getCountry().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
Avatar avatar = null;
|
||||
if (sharedContact.getAvatar().isPresent()) {
|
||||
Attachment attachment = PointerAttachment.forPointer(Optional.of(sharedContact.getAvatar().get().getAttachment().asPointer())).get();
|
||||
boolean isProfile = sharedContact.getAvatar().get().isProfile();
|
||||
|
||||
avatar = new Avatar(null, attachment, isProfile);
|
||||
}
|
||||
|
||||
return new Contact(name, sharedContact.getOrganization().orNull(), phoneNumbers, emails, postalAddresses, avatar);
|
||||
}
|
||||
|
||||
private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Phone.Type.HOME;
|
||||
case MOBILE: return Phone.Type.MOBILE;
|
||||
case WORK: return Phone.Type.WORK;
|
||||
default: return Phone.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static Email.Type remoteToLocalType(SharedContact.Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Email.Type.HOME;
|
||||
case MOBILE: return Email.Type.MOBILE;
|
||||
case WORK: return Email.Type.WORK;
|
||||
default: return Email.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static PostalAddress.Type remoteToLocalType(SharedContact.PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return PostalAddress.Type.HOME;
|
||||
case WORK: return PostalAddress.Type.WORK;
|
||||
default: return PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.Phone.Type localToRemoteType(Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.Phone.Type.HOME;
|
||||
case MOBILE: return SharedContact.Phone.Type.MOBILE;
|
||||
case WORK: return SharedContact.Phone.Type.WORK;
|
||||
default: return SharedContact.Phone.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.Email.Type localToRemoteType(Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.Email.Type.HOME;
|
||||
case MOBILE: return SharedContact.Email.Type.MOBILE;
|
||||
case WORK: return SharedContact.Email.Type.WORK;
|
||||
default: return SharedContact.Email.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.PostalAddress.Type localToRemoteType(PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.PostalAddress.Type.HOME;
|
||||
case WORK: return SharedContact.PostalAddress.Type.WORK;
|
||||
default: return SharedContact.PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,14 +32,13 @@ class ContactListAdapter(
|
|||
|
||||
class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) {
|
||||
binding.profilePictureView.root.glide = glide
|
||||
binding.profilePictureView.root.update(contact.recipient)
|
||||
binding.profilePictureView.update(contact.recipient)
|
||||
binding.nameTextView.text = contact.displayName
|
||||
binding.root.setOnClickListener { listener(contact.recipient) }
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
binding.profilePictureView.root.recycle()
|
||||
binding.profilePictureView.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ class NewConversationHomeFragment : Fragment() {
|
|||
val displayName = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionId
|
||||
ContactListItem.Contact(it, displayName)
|
||||
}.sortedBy { it.displayName }
|
||||
.groupBy { if (PublicKeyValidation.isValid(it.displayName)) unknownSectionTitle else it.displayName.first().uppercase() }
|
||||
.groupBy { if (PublicKeyValidation.isValid(it.displayName)) unknownSectionTitle else it.displayName.firstOrNull()?.uppercase() ?: unknownSectionTitle }
|
||||
.toMutableMap()
|
||||
contactGroups.remove(unknownSectionTitle)?.let { contactGroups.put(unknownSectionTitle, it) }
|
||||
adapter.items = contactGroups.flatMap { entry -> listOf(ContactListItem.Header(entry.key)) + entry.value }
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,5 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
|
@ -31,10 +30,15 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.min
|
||||
|
||||
class ConversationAdapter(
|
||||
context: Context,
|
||||
cursor: Cursor,
|
||||
originalLastSeen: Long,
|
||||
private val isReversed: Boolean,
|
||||
private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
|
||||
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit,
|
||||
private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit,
|
||||
|
@ -52,6 +56,8 @@ class ConversationAdapter(
|
|||
private val updateQueue = Channel<String>(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
private val contactCache = SparseArray<Contact>(100)
|
||||
private val contactLoadedCache = SparseBooleanArray(100)
|
||||
private val lastSeen = AtomicLong(originalLastSeen)
|
||||
|
||||
init {
|
||||
lifecycleCoroutineScope.launch(IO) {
|
||||
while (isActive) {
|
||||
|
@ -128,6 +134,7 @@ class ConversationAdapter(
|
|||
searchQuery,
|
||||
contact,
|
||||
senderId,
|
||||
lastSeen.get(),
|
||||
visibleMessageViewDelegate,
|
||||
onAttachmentNeedsDownload
|
||||
)
|
||||
|
@ -146,17 +153,15 @@ class ConversationAdapter(
|
|||
viewHolder.view.bind(message, messageBefore)
|
||||
if (message.isCallLog && message.isFirstMissedCall) {
|
||||
viewHolder.view.setOnClickListener {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.CallNotificationBuilder_first_call_title)
|
||||
.setMessage(R.string.CallNotificationBuilder_first_call_message)
|
||||
.setPositiveButton(R.string.activity_settings_title) { _, _ ->
|
||||
val intent = Intent(context, PrivacySettingsActivity::class.java)
|
||||
context.startActivity(intent)
|
||||
context.showSessionDialog {
|
||||
title(R.string.CallNotificationBuilder_first_call_title)
|
||||
text(R.string.CallNotificationBuilder_first_call_message)
|
||||
button(R.string.activity_settings_title) {
|
||||
Intent(context, PrivacySettingsActivity::class.java)
|
||||
.let(context::startActivity)
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { d, _ ->
|
||||
d.dismiss()
|
||||
}
|
||||
.show()
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewHolder.view.setOnClickListener(null)
|
||||
|
@ -185,14 +190,18 @@ class ConversationAdapter(
|
|||
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
|
||||
// The message that's visually before the current one is actually after the current
|
||||
// one for the cursor because the layout is reversed
|
||||
if (!cursor.moveToPosition(position + 1)) { return null }
|
||||
if (isReversed && !cursor.moveToPosition(position + 1)) { return null }
|
||||
if (!isReversed && !cursor.moveToPosition(position - 1)) { return null }
|
||||
|
||||
return messageDB.readerFor(cursor).current
|
||||
}
|
||||
|
||||
private fun getMessageAfter(position: Int, cursor: Cursor): MessageRecord? {
|
||||
// The message that's visually after the current one is actually before the current
|
||||
// one for the cursor because the layout is reversed
|
||||
if (!cursor.moveToPosition(position - 1)) { return null }
|
||||
if (isReversed && !cursor.moveToPosition(position - 1)) { return null }
|
||||
if (!isReversed && !cursor.moveToPosition(position + 1)) { return null }
|
||||
|
||||
return messageDB.readerFor(cursor).current
|
||||
}
|
||||
|
||||
|
@ -219,11 +228,30 @@ class ConversationAdapter(
|
|||
|
||||
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
|
||||
val cursor = this.cursor
|
||||
if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null
|
||||
if (cursor == null || !isActiveCursor) return null
|
||||
if (lastSeenTimestamp == 0L) {
|
||||
if (isReversed && cursor.moveToLast()) { return cursor.position }
|
||||
if (!isReversed && cursor.moveToFirst()) { return cursor.position }
|
||||
}
|
||||
|
||||
// Loop from the newest message to the oldest until we find one older (or equal to)
|
||||
// the lastSeenTimestamp, then return that message index
|
||||
for (i in 0 until itemCount) {
|
||||
cursor.moveToPosition(i)
|
||||
val message = messageDB.readerFor(cursor).current
|
||||
if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i }
|
||||
if (isReversed) {
|
||||
cursor.moveToPosition(i)
|
||||
val (outgoing, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
|
||||
if (outgoing || dateSent <= lastSeenTimestamp) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
else {
|
||||
val index = ((itemCount - 1) - i)
|
||||
cursor.moveToPosition(index)
|
||||
val (outgoing, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
|
||||
if (outgoing || dateSent <= lastSeenTimestamp) {
|
||||
return min(itemCount - 1, (index + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -233,8 +261,8 @@ class ConversationAdapter(
|
|||
if (timestamp <= 0L || cursor == null || !isActiveCursor) return null
|
||||
for (i in 0 until itemCount) {
|
||||
cursor.moveToPosition(i)
|
||||
val message = messageDB.readerFor(cursor).current
|
||||
if (message.dateSent == timestamp) { return i }
|
||||
val (_, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
|
||||
if (dateSent == timestamp) { return i }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -243,4 +271,11 @@ class ConversationAdapter(
|
|||
this.searchQuery = query
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun getTimestampForItemAt(firstVisiblePosition: Int): Long? {
|
||||
val cursor = this.cursor ?: return null
|
||||
if (!cursor.moveToPosition(firstVisiblePosition)) return null
|
||||
val message = messageDB.readerFor(cursor).current ?: return null
|
||||
return message.timestamp
|
||||
}
|
||||
}
|
|
@ -81,6 +81,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
|
||||
private View dropdownAnchor;
|
||||
private LinearLayout conversationItem;
|
||||
private View conversationBubble;
|
||||
private TextView conversationTimestamp;
|
||||
private View backgroundView;
|
||||
private ConstraintLayout foregroundView;
|
||||
private EmojiImageView[] emojiViews;
|
||||
|
@ -116,6 +118,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
|
||||
dropdownAnchor = findViewById(R.id.dropdown_anchor);
|
||||
conversationItem = findViewById(R.id.conversation_item);
|
||||
conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble);
|
||||
conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
|
||||
|
||||
|
@ -165,10 +169,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
|
||||
View conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble);
|
||||
conversationBubble.setLayoutParams(new LinearLayout.LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
|
||||
conversationBubble.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));
|
||||
TextView conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
|
||||
conversationTimestamp.setText(DateUtils.getDisplayFormattedTimeSpanString(getContext(), Locale.getDefault(), messageRecord.getTimestamp()));
|
||||
|
||||
updateConversationTimestamp(messageRecord);
|
||||
|
@ -190,12 +192,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
}
|
||||
|
||||
private void updateConversationTimestamp(MessageRecord message) {
|
||||
View bubble = conversationItem.findViewById(R.id.conversation_item_bubble);
|
||||
View timestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
|
||||
conversationItem.removeAllViewsInLayout();
|
||||
conversationItem.addView(message.isOutgoing() ? timestamp : bubble);
|
||||
conversationItem.addView(message.isOutgoing() ? bubble : timestamp);
|
||||
conversationItem.requestLayout();
|
||||
if (message.isOutgoing()) conversationBubble.bringToFront();
|
||||
else conversationTimestamp.bringToFront();
|
||||
}
|
||||
|
||||
private void showAfterLayout(@NonNull MessageRecord messageRecord,
|
||||
|
@ -203,10 +201,11 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
boolean isMessageOnLeft) {
|
||||
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(messageRecord));
|
||||
|
||||
float itemX = isMessageOnLeft ? scrubberHorizontalMargin :
|
||||
float endX = isMessageOnLeft ? scrubberHorizontalMargin :
|
||||
selectedConversationModel.getBubbleX() - conversationItem.getWidth() + selectedConversationModel.getBubbleWidth();
|
||||
conversationItem.setX(itemX);
|
||||
conversationItem.setY(selectedConversationModel.getBubbleY() - statusBarHeight);
|
||||
float endY = selectedConversationModel.getBubbleY() - statusBarHeight;
|
||||
conversationItem.setX(endX);
|
||||
conversationItem.setY(endY);
|
||||
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth();
|
||||
|
@ -214,8 +213,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
int overlayHeight = getHeight();
|
||||
int bubbleWidth = selectedConversationModel.getBubbleWidth();
|
||||
|
||||
float endX = itemX;
|
||||
float endY = conversationItem.getY();
|
||||
float endApparentTop = endY;
|
||||
float endScale = 1f;
|
||||
|
||||
|
@ -265,9 +262,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
}
|
||||
} else {
|
||||
endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight();
|
||||
|
||||
float contextMenuTop = endY + conversationItemSnapshot.getHeight();
|
||||
reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
|
||||
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding;
|
||||
}
|
||||
|
||||
endApparentTop = endY;
|
||||
|
@ -354,11 +349,14 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
|
||||
int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
|
||||
|
||||
conversationBubble.animate()
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale)
|
||||
.setDuration(revealDuration);
|
||||
|
||||
conversationItem.animate()
|
||||
.x(endX)
|
||||
.y(endY)
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale)
|
||||
.setDuration(revealDuration);
|
||||
}
|
||||
|
||||
|
@ -663,7 +661,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
items.add(new ActionItem(R.attr.menu_select_icon, getContext().getResources().getString(R.string.conversation_context__menu_select), () -> handleActionItemClicked(Action.SELECT),
|
||||
getContext().getResources().getString(R.string.AccessibilityId_select)));
|
||||
// Reply
|
||||
if (!message.isPending() && !message.isFailed()) {
|
||||
boolean canWrite = openGroup == null || openGroup.getCanWrite();
|
||||
if (canWrite && !message.isPending() && !message.isFailed()) {
|
||||
items.add(
|
||||
new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_reply), () -> handleActionItemClicked(Action.REPLY),
|
||||
getContext().getResources().getString(R.string.AccessibilityId_reply_message))
|
||||
|
@ -696,13 +695,15 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_and_delete_all), () -> handleActionItemClicked(Action.BAN_AND_DELETE_ALL)));
|
||||
}
|
||||
// Message detail
|
||||
if (message.isFailed()) {
|
||||
items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
|
||||
}
|
||||
items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
|
||||
// Resend
|
||||
if (message.isFailed()) {
|
||||
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
|
||||
}
|
||||
// Resync
|
||||
if (message.isSyncFailed()) {
|
||||
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resync_message), () -> handleActionItemClicked(Action.RESYNC)));
|
||||
}
|
||||
// Save media
|
||||
if (message.isMms() && ((MediaMmsMessageRecord)message).containsMediaSlide()) {
|
||||
items.add(new ActionItem(R.attr.menu_save_icon, getContext().getResources().getString(R.string.conversation_context_image__save_attachment), () -> handleActionItemClicked(Action.DOWNLOAD),
|
||||
|
@ -888,6 +889,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
public enum Action {
|
||||
REPLY,
|
||||
RESEND,
|
||||
RESYNC,
|
||||
DOWNLOAD,
|
||||
COPY_MESSAGE,
|
||||
COPY_SESSION_ID,
|
||||
|
|
|
@ -31,7 +31,10 @@ class ConversationViewModel(
|
|||
private val storage: Storage
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ConversationUiState())
|
||||
val showSendAfterApprovalText: Boolean
|
||||
get() = recipient?.run { isContactRecipient && !isLocalNumber && !hasApprovedMe() } ?: false
|
||||
|
||||
private val _uiState = MutableStateFlow(ConversationUiState(conversationExists = true))
|
||||
val uiState: StateFlow<ConversationUiState> = _uiState
|
||||
|
||||
private var _recipient: RetrieveOnce<Recipient> = RetrieveOnce {
|
||||
|
@ -40,6 +43,15 @@ class ConversationViewModel(
|
|||
val recipient: Recipient?
|
||||
get() = _recipient.value
|
||||
|
||||
val blindedRecipient: Recipient?
|
||||
get() = _recipient.value?.let { recipient ->
|
||||
when {
|
||||
recipient.isOpenGroupOutboxRecipient -> recipient
|
||||
recipient.isOpenGroupInboxRecipient -> repository.maybeGetBlindedRecipient(recipient)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private var _openGroup: RetrieveOnce<OpenGroup> = RetrieveOnce {
|
||||
storage.getOpenGroup(threadId)
|
||||
}
|
||||
|
@ -55,6 +67,28 @@ class ConversationViewModel(
|
|||
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
|
||||
}
|
||||
|
||||
val isMessageRequestThread : Boolean
|
||||
get() {
|
||||
val recipient = recipient ?: return false
|
||||
return !recipient.isLocalNumber && !recipient.isGroupRecipient && !recipient.isApproved
|
||||
}
|
||||
|
||||
val canReactToMessages: Boolean
|
||||
// allow reactions if the open group is null (normal conversations) or the open group's capabilities include reactions
|
||||
get() = (openGroup == null || OpenGroupApi.Capability.REACTIONS.name.lowercase() in serverCapabilities)
|
||||
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.recipientUpdateFlow(threadId)
|
||||
.collect { recipient ->
|
||||
if (recipient == null && _uiState.value.conversationExists) {
|
||||
_uiState.update { it.copy(conversationExists = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDraft(text: String) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
repository.saveDraft(threadId, text)
|
||||
|
@ -180,6 +214,10 @@ class ConversationViewModel(
|
|||
_recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId))
|
||||
}
|
||||
|
||||
fun hidesInputBar(): Boolean = openGroup?.canWrite != true &&
|
||||
blindedRecipient?.blocksCommunityMessageRequests == true
|
||||
|
||||
|
||||
@dagger.assisted.AssistedFactory
|
||||
interface AssistedFactory {
|
||||
fun create(threadId: Long, edKeyPair: KeyPair?): Factory
|
||||
|
@ -203,7 +241,8 @@ data class UiMessage(val id: Long, val message: String)
|
|||
|
||||
data class ConversationUiState(
|
||||
val uiMessages: List<UiMessage> = emptyList(),
|
||||
val isMessageRequestAccepted: Boolean? = null
|
||||
val isMessageRequestAccepted: Boolean? = null,
|
||||
val conversationExists: Boolean
|
||||
)
|
||||
|
||||
data class RetrieveOnce<T>(val retrieval: () -> T?) {
|
||||
|
|
|
@ -69,7 +69,6 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
|||
override fun onStart() {
|
||||
super.onStart()
|
||||
val window = dialog?.window ?: return
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(requireContext())
|
||||
window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
|
||||
window.setDimAmount(0.6f)
|
||||
}
|
||||
}
|
|
@ -1,99 +1,401 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent.ACTION_UP
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||
import com.bumptech.glide.integration.compose.GlideImage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityMessageDetailBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import org.thoughtcrime.securesms.ui.AppTheme
|
||||
import org.thoughtcrime.securesms.ui.Avatar
|
||||
import org.thoughtcrime.securesms.ui.CarouselNextButton
|
||||
import org.thoughtcrime.securesms.ui.CarouselPrevButton
|
||||
import org.thoughtcrime.securesms.ui.Cell
|
||||
import org.thoughtcrime.securesms.ui.CellNoMargin
|
||||
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
|
||||
import org.thoughtcrime.securesms.ui.Divider
|
||||
import org.thoughtcrime.securesms.ui.GetString
|
||||
import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
|
||||
import org.thoughtcrime.securesms.ui.ItemButton
|
||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||
import org.thoughtcrime.securesms.ui.TitledText
|
||||
import org.thoughtcrime.securesms.ui.blackAlpha40
|
||||
import org.thoughtcrime.securesms.ui.colorDestructive
|
||||
import org.thoughtcrime.securesms.ui.destructiveButtonColors
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
private lateinit var binding: ActivityMessageDetailBinding
|
||||
var messageRecord: MessageRecord? = null
|
||||
class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var storage: Storage
|
||||
|
||||
// region Settings
|
||||
private val viewModel: MessageDetailsViewModel by viewModels()
|
||||
|
||||
companion object {
|
||||
// Extras
|
||||
const val MESSAGE_TIMESTAMP = "message_timestamp"
|
||||
|
||||
const val ON_REPLY = 1
|
||||
const val ON_RESEND = 2
|
||||
const val ON_DELETE = 3
|
||||
}
|
||||
// endregion
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
binding = ActivityMessageDetailBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
title = resources.getString(R.string.conversation_context__menu_message_details)
|
||||
val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
||||
// We only show this screen for messages fail to send,
|
||||
// so the author of the messages must be the current user.
|
||||
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
|
||||
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
val threadId = messageRecord!!.threadId
|
||||
val openGroup = storage.getOpenGroup(threadId)
|
||||
val blindedKey = openGroup?.let { group ->
|
||||
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return@let null
|
||||
val blindingEnabled = storage.getServerCapabilities(group.server).contains(OpenGroupApi.Capability.BLIND.name.lowercase())
|
||||
if (blindingEnabled) {
|
||||
SodiumUtilities.blindedKeyPair(group.publicKey, userEdKeyPair)?.publicKey?.asBytes
|
||||
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
|
||||
} else null
|
||||
}
|
||||
updateContent()
|
||||
binding.resendButton.setOnClickListener {
|
||||
ResendMessageUtilities.resend(this, messageRecord!!, blindedKey)
|
||||
finish()
|
||||
|
||||
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
||||
|
||||
ComposeView(this)
|
||||
.apply { setContent { MessageDetailsScreen() } }
|
||||
.let(::setContentView)
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.eventFlow.collect {
|
||||
when (it) {
|
||||
Event.Finish -> finish()
|
||||
is Event.StartMediaPreview -> startActivity(
|
||||
getPreviewIntent(this@MessageDetailActivity, it.args)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContent() {
|
||||
val dateLocale = Locale.getDefault()
|
||||
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
|
||||
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
|
||||
|
||||
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId())
|
||||
if (errorMessage != null) {
|
||||
binding.errorMessage.text = errorMessage
|
||||
binding.resendContainer.isVisible = true
|
||||
binding.errorContainer.isVisible = true
|
||||
} else {
|
||||
binding.errorContainer.isVisible = false
|
||||
binding.resendContainer.isVisible = false
|
||||
}
|
||||
|
||||
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
|
||||
binding.expiresContainer.visibility = View.GONE
|
||||
} else {
|
||||
binding.expiresContainer.visibility = View.VISIBLE
|
||||
val elapsed = SnodeAPI.nowWithOffset - messageRecord!!.expireStarted
|
||||
val remaining = messageRecord!!.expiresIn - elapsed
|
||||
|
||||
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
|
||||
binding.expiresIn.text = duration
|
||||
@Composable
|
||||
private fun MessageDetailsScreen() {
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
AppTheme {
|
||||
MessageDetails(
|
||||
state = state,
|
||||
onReply = { setResultAndFinish(ON_REPLY) },
|
||||
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
||||
onDelete = { setResultAndFinish(ON_DELETE) },
|
||||
onClickImage = { viewModel.onClickImage(it) },
|
||||
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setResultAndFinish(code: Int) {
|
||||
Bundle().apply { putLong(MESSAGE_TIMESTAMP, viewModel.timestamp) }
|
||||
.let(Intent()::putExtras)
|
||||
.let { setResult(code, it) }
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Composable
|
||||
fun MessageDetails(
|
||||
state: MessageDetailsState,
|
||||
onReply: () -> Unit = {},
|
||||
onResend: (() -> Unit)? = null,
|
||||
onDelete: () -> Unit = {},
|
||||
onClickImage: (Int) -> Unit = {},
|
||||
onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
state.record?.let { message ->
|
||||
AndroidView(
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
factory = {
|
||||
ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply {
|
||||
bind(
|
||||
message,
|
||||
thread = state.thread!!,
|
||||
onAttachmentNeedsDownload = onAttachmentNeedsDownload,
|
||||
suppressThumbnails = true
|
||||
)
|
||||
|
||||
setOnTouchListener { _, event ->
|
||||
if (event.actionMasked == ACTION_UP) onContentClick(event)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Carousel(state.imageAttachments) { onClickImage(it) }
|
||||
state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
|
||||
CellMetadata(state)
|
||||
CellButtons(
|
||||
onReply,
|
||||
onResend,
|
||||
onDelete,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CellMetadata(
|
||||
state: MessageDetailsState,
|
||||
) {
|
||||
state.apply {
|
||||
if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
|
||||
CellWithPaddingAndMargin {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
TitledText(sent)
|
||||
TitledText(received)
|
||||
TitledErrorText(error)
|
||||
senderInfo?.let {
|
||||
TitledView(state.fromTitle) {
|
||||
Row {
|
||||
sender?.let { Avatar(it) }
|
||||
TitledMonospaceText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CellButtons(
|
||||
onReply: () -> Unit = {},
|
||||
onResend: (() -> Unit)? = null,
|
||||
onDelete: () -> Unit = {},
|
||||
) {
|
||||
Cell {
|
||||
Column {
|
||||
ItemButton(
|
||||
stringResource(R.string.reply),
|
||||
R.drawable.ic_message_details__reply,
|
||||
onClick = onReply
|
||||
)
|
||||
Divider()
|
||||
onResend?.let {
|
||||
ItemButton(
|
||||
stringResource(R.string.resend),
|
||||
R.drawable.ic_message_details__refresh,
|
||||
onClick = it
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
ItemButton(
|
||||
stringResource(R.string.delete),
|
||||
R.drawable.ic_message_details__trash,
|
||||
colors = destructiveButtonColors(),
|
||||
onClick = onDelete
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun Carousel(attachments: List<Attachment>, onClick: (Int) -> Unit) {
|
||||
if (attachments.isEmpty()) return
|
||||
|
||||
val pagerState = rememberPagerState { attachments.size }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Row {
|
||||
CarouselPrevButton(pagerState)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
CellCarousel(pagerState, attachments, onClick)
|
||||
HorizontalPagerIndicator(pagerState)
|
||||
ExpandButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(8.dp)
|
||||
) { onClick(pagerState.currentPage) }
|
||||
}
|
||||
CarouselNextButton(pagerState)
|
||||
}
|
||||
attachments.getOrNull(pagerState.currentPage)?.fileDetails?.let { FileDetails(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(
|
||||
ExperimentalFoundationApi::class,
|
||||
ExperimentalGlideComposeApi::class
|
||||
)
|
||||
@Composable
|
||||
private fun CellCarousel(
|
||||
pagerState: PagerState,
|
||||
attachments: List<Attachment>,
|
||||
onClick: (Int) -> Unit
|
||||
) {
|
||||
CellNoMargin {
|
||||
HorizontalPager(state = pagerState) { i ->
|
||||
GlideImage(
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f)
|
||||
.clickable { onClick(i) },
|
||||
model = attachments[i].uri,
|
||||
contentDescription = attachments[i].fileName ?: stringResource(id = R.string.image)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = blackAlpha40,
|
||||
modifier = modifier,
|
||||
contentColor = Color.White,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_expand),
|
||||
contentDescription = stringResource(id = R.string.expand),
|
||||
modifier = Modifier.clickable { onClick() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewMessageDetails(
|
||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||
) {
|
||||
PreviewTheme(themeResId) {
|
||||
MessageDetails(
|
||||
state = MessageDetailsState(
|
||||
nonImageAttachmentFileDetails = listOf(
|
||||
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
|
||||
TitledText(R.string.message_details_header__file_type, "image/png"),
|
||||
TitledText(R.string.message_details_header__file_size, "195.6kB"),
|
||||
TitledText(R.string.message_details_header__resolution, "342x312"),
|
||||
),
|
||||
sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"),
|
||||
received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"),
|
||||
error = TitledText(R.string.message_details_header__error, "Message failed to send"),
|
||||
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun FileDetails(fileDetails: List<TitledText>) {
|
||||
if (fileDetails.isEmpty()) return
|
||||
|
||||
CellWithPaddingAndMargin(padding = 0.dp) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(vertical = 24.dp, horizontal = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
fileDetails.forEach {
|
||||
BoxWithConstraints {
|
||||
TitledText(
|
||||
it,
|
||||
modifier = Modifier
|
||||
.widthIn(min = maxWidth.div(2))
|
||||
.padding(horizontal = 12.dp)
|
||||
.width(IntrinsicSize.Max)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitledErrorText(titledText: TitledText?) {
|
||||
TitledText(
|
||||
titledText,
|
||||
valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitledMonospaceText(titledText: TitledText?) {
|
||||
TitledText(
|
||||
titledText,
|
||||
valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitledText(
|
||||
titledText: TitledText?,
|
||||
modifier: Modifier = Modifier,
|
||||
valueStyle: TextStyle = LocalTextStyle.current,
|
||||
) {
|
||||
titledText?.apply {
|
||||
TitledView(title, modifier) {
|
||||
Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Title(title)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Title(title: GetString) {
|
||||
Text(title.string(), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.MediaPreviewArgs
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.ui.GetString
|
||||
import org.thoughtcrime.securesms.ui.TitledText
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MessageDetailsViewModel @Inject constructor(
|
||||
private val attachmentDb: AttachmentDatabase,
|
||||
private val lokiMessageDatabase: LokiMessageDatabase,
|
||||
private val mmsSmsDatabase: MmsSmsDatabase,
|
||||
private val threadDb: ThreadDatabase,
|
||||
) : ViewModel() {
|
||||
|
||||
private val state = MutableStateFlow(MessageDetailsState())
|
||||
val stateFlow = state.asStateFlow()
|
||||
|
||||
private val event = Channel<Event>()
|
||||
val eventFlow = event.receiveAsFlow()
|
||||
|
||||
var timestamp: Long = 0L
|
||||
set(value) {
|
||||
field = value
|
||||
val record = mmsSmsDatabase.getMessageForTimestamp(timestamp)
|
||||
|
||||
if (record == null) {
|
||||
viewModelScope.launch { event.send(Event.Finish) }
|
||||
return
|
||||
}
|
||||
|
||||
val mmsRecord = record as? MmsMessageRecord
|
||||
|
||||
state.value = record.run {
|
||||
val slides = mmsRecord?.slideDeck?.slides ?: emptyList()
|
||||
|
||||
MessageDetailsState(
|
||||
attachments = slides.map(::Attachment),
|
||||
record = record,
|
||||
sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) },
|
||||
received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) },
|
||||
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) },
|
||||
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
|
||||
sender = individualRecipient,
|
||||
thread = threadDb.getRecipientForThreadId(threadId)!!,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val Slide.details: List<TitledText>
|
||||
get() = listOfNotNull(
|
||||
fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) },
|
||||
TitledText(R.string.message_details_header__file_type, asAttachment().contentType),
|
||||
TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)),
|
||||
takeIf { it is ImageSlide }
|
||||
?.let(Slide::asAttachment)
|
||||
?.run { "${width}x$height" }
|
||||
?.let { TitledText(R.string.message_details_header__resolution, it) },
|
||||
attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) },
|
||||
)
|
||||
|
||||
private fun AttachmentDatabase.duration(slide: Slide): String? =
|
||||
slide.takeIf { it.hasAudio() }
|
||||
?.run { asAttachment() as? DatabaseAttachment }
|
||||
?.run { getAttachmentAudioExtras(attachmentId)?.durationMs }
|
||||
?.takeIf { it > 0 }
|
||||
?.let {
|
||||
String.format(
|
||||
"%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(it),
|
||||
TimeUnit.MILLISECONDS.toSeconds(it) % 60
|
||||
)
|
||||
}
|
||||
|
||||
fun Attachment(slide: Slide): Attachment =
|
||||
Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
|
||||
|
||||
fun onClickImage(index: Int) {
|
||||
val state = state.value ?: return
|
||||
val mmsRecord = state.mmsRecord ?: return
|
||||
val slide = mmsRecord.slideDeck.slides[index] ?: return
|
||||
// only open to downloaded images
|
||||
if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
|
||||
// Restart download here (on IO thread)
|
||||
(slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
|
||||
onAttachmentNeedsDownload(attachment.attachmentId.rowId, state.mmsRecord.getId())
|
||||
}
|
||||
}
|
||||
|
||||
if (slide.isInProgress) return
|
||||
|
||||
viewModelScope.launch {
|
||||
MediaPreviewArgs(slide, state.mmsRecord, state.thread)
|
||||
.let(Event::StartMediaPreview)
|
||||
.let { event.send(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageDetailsState(
|
||||
val attachments: List<Attachment> = emptyList(),
|
||||
val imageAttachments: List<Attachment> = attachments.filter { it.hasImage },
|
||||
val nonImageAttachmentFileDetails: List<TitledText>? = attachments.firstOrNull { !it.hasImage }?.fileDetails,
|
||||
val record: MessageRecord? = null,
|
||||
val mmsRecord: MmsMessageRecord? = record as? MmsMessageRecord,
|
||||
val sent: TitledText? = null,
|
||||
val received: TitledText? = null,
|
||||
val error: TitledText? = null,
|
||||
val senderInfo: TitledText? = null,
|
||||
val sender: Recipient? = null,
|
||||
val thread: Recipient? = null,
|
||||
) {
|
||||
val fromTitle = GetString(R.string.message_details_header__from)
|
||||
}
|
||||
|
||||
data class Attachment(
|
||||
val fileDetails: List<TitledText>,
|
||||
val fileName: String?,
|
||||
val uri: Uri?,
|
||||
val hasImage: Boolean
|
||||
)
|
||||
|
||||
sealed class Event {
|
||||
object Finish: Event()
|
||||
data class StartMediaPreview(val args: MediaPreviewArgs): Event()
|
||||
}
|
|
@ -60,8 +60,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
|||
override fun onStart() {
|
||||
super.onStart()
|
||||
val window = dialog?.window ?: return
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(requireContext())
|
||||
window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
|
||||
window.setDimAmount(0.6f)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
|
|
|
@ -38,14 +38,10 @@ public final class WindowUtil {
|
|||
}
|
||||
|
||||
public static void setNavigationBarColor(@NonNull Window window, @ColorInt int color) {
|
||||
if (Build.VERSION.SDK_INT < 21) return;
|
||||
|
||||
window.setNavigationBarColor(color);
|
||||
}
|
||||
|
||||
public static void setLightStatusBarFromTheme(@NonNull Activity activity) {
|
||||
if (Build.VERSION.SDK_INT < 23) return;
|
||||
|
||||
final boolean isLightStatusBar = ThemeUtil.getThemedBoolean(activity, android.R.attr.windowLightStatusBar);
|
||||
|
||||
if (isLightStatusBar) setLightStatusBar(activity.getWindow());
|
||||
|
@ -53,20 +49,14 @@ public final class WindowUtil {
|
|||
}
|
||||
|
||||
public static void clearLightStatusBar(@NonNull Window window) {
|
||||
if (Build.VERSION.SDK_INT < 23) return;
|
||||
|
||||
clearSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
|
||||
public static void setLightStatusBar(@NonNull Window window) {
|
||||
if (Build.VERSION.SDK_INT < 23) return;
|
||||
|
||||
setSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
|
||||
public static void setStatusBarColor(@NonNull Window window, @ColorInt int color) {
|
||||
if (Build.VERSION.SDK_INT < 21) return;
|
||||
|
||||
window.setStatusBarColor(color);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.util.AttributeSet
|
|||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
|
@ -41,7 +40,7 @@ class AlbumThumbnailView : RelativeLayout {
|
|||
private var slides: List<Slide> = listOf()
|
||||
private var slideSize: Int = 0
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas?) {
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
super.dispatchDraw(canvas)
|
||||
cornerMask.mask(canvas)
|
||||
}
|
||||
|
|
|
@ -28,11 +28,10 @@ class MentionCandidateView : LinearLayout {
|
|||
|
||||
private fun update() = with(binding) {
|
||||
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||
profilePictureView.root.publicKey = mentionCandidate.publicKey
|
||||
profilePictureView.root.displayName = mentionCandidate.displayName
|
||||
profilePictureView.root.additionalPublicKey = null
|
||||
profilePictureView.root.glide = glide!!
|
||||
profilePictureView.root.update()
|
||||
profilePictureView.publicKey = mentionCandidate.publicKey
|
||||
profilePictureView.displayName = mentionCandidate.displayName
|
||||
profilePictureView.additionalPublicKey = null
|
||||
profilePictureView.update()
|
||||
if (openGroupServer != null && openGroupRoom != null) {
|
||||
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
|
||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||
|
|
|
@ -1,41 +1,42 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogBlockedBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
|
||||
/** Shown upon sending a message to a user that's blocked. */
|
||||
class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
|
||||
val sessionID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
val title = resources.getString(R.string.dialog_blocked_title, name)
|
||||
binding.blockedTitleTextView.text = title
|
||||
|
||||
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
binding.blockedExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.unblockButton.setOnClickListener { unblock() }
|
||||
builder.setView(binding.root)
|
||||
|
||||
title(resources.getString(R.string.dialog_blocked_title, name))
|
||||
text(spannable)
|
||||
button(R.string.ConversationActivity_unblock) { unblock() }
|
||||
cancelButton { dismiss() }
|
||||
}
|
||||
|
||||
private fun unblock() {
|
||||
DatabaseComponent.get(requireContext()).recipientDatabase().setBlocked(recipient, false)
|
||||
MessagingModuleConfiguration.shared.storage.setBlocked(listOf(recipient), false)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogDownloadBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import javax.inject.Inject
|
||||
|
@ -21,25 +21,24 @@ import javax.inject.Inject
|
|||
/** Shown when receiving media from a contact for the first time, to confirm that
|
||||
* they are to be trusted and files sent by them are to be downloaded. */
|
||||
@AndroidEntryPoint
|
||||
class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
|
||||
|
||||
@Inject lateinit var contactDB: SessionContactDatabase
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val binding = DialogDownloadBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
val sessionID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
val title = resources.getString(R.string.dialog_download_title, name)
|
||||
binding.downloadTitleTextView.text = title
|
||||
title(resources.getString(R.string.dialog_download_title, name))
|
||||
|
||||
val explanation = resources.getString(R.string.dialog_download_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
binding.downloadExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.downloadButton.setOnClickListener { trust() }
|
||||
builder.setView(binding.root)
|
||||
text(spannable)
|
||||
|
||||
button(R.string.dialog_download_button_title, R.string.AccessibilityId_download_media) { trust() }
|
||||
cancelButton { dismiss() }
|
||||
}
|
||||
|
||||
private fun trust() {
|
||||
|
@ -50,4 +49,4 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
|
|||
JobQueue.shared.resumePendingJobs(AttachmentDownloadJob.KEY)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,42 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
|
||||
/** Shown upon tapping an open group invitation. */
|
||||
class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
|
||||
class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val binding = DialogJoinOpenGroupBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
val title = resources.getString(R.string.dialog_join_open_group_title, name)
|
||||
binding.joinOpenGroupTitleTextView.text = title
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
title(resources.getString(R.string.dialog_join_open_group_title, name))
|
||||
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
binding.joinOpenGroupExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.joinButton.setOnClickListener { join() }
|
||||
builder.setView(binding.root)
|
||||
text(spannable)
|
||||
cancelButton { dismiss() }
|
||||
button(R.string.open_group_invitation_view__join_accessibility_description) { join() }
|
||||
}
|
||||
|
||||
private fun join() {
|
||||
val openGroup = OpenGroupUrlParser.parseUrl(url)
|
||||
val activity = requireContext() as AppCompatActivity
|
||||
val activity = requireActivity()
|
||||
ThreadUtils.queue {
|
||||
try {
|
||||
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
|
||||
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server)
|
||||
openGroup.apply { OpenGroupManager.add(server, room, serverPublicKey, activity) }
|
||||
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
|
@ -48,4 +44,4 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
|
|||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import network.loki.messenger.databinding.DialogLinkPreviewBinding
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
|
||||
/** Shown the first time the user inputs a URL that could generate a link preview, to
|
||||
* let them know that Session offers the ability to send and receive link previews. */
|
||||
class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
|
||||
class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val binding = DialogLinkPreviewBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.enableLinkPreviewsButton.setOnClickListener { enable() }
|
||||
builder.setView(binding.root)
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
title(R.string.dialog_link_preview_title)
|
||||
text(R.string.dialog_link_preview_explanation)
|
||||
button(R.string.dialog_link_preview_enable_button_title) { enable() }
|
||||
cancelButton { dismiss() }
|
||||
}
|
||||
|
||||
private fun enable() {
|
||||
|
@ -22,4 +23,4 @@ class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
|
|||
dismiss()
|
||||
onEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import network.loki.messenger.databinding.DialogSendSeedBinding
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
|
||||
/** Shown if the user is about to send their recovery phrase to someone. */
|
||||
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
|
||||
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.sendSeedButton.setOnClickListener { send() }
|
||||
builder.setView(binding.root)
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
title(R.string.dialog_send_seed_title)
|
||||
text(R.string.dialog_send_seed_explanation)
|
||||
button(R.string.dialog_send_seed_send_button_title) { send() }
|
||||
cancelButton()
|
||||
}
|
||||
|
||||
private fun send() {
|
||||
proceed?.invoke()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,11 +28,10 @@ class MentionCandidateView : RelativeLayout {
|
|||
|
||||
private fun update() = with(binding) {
|
||||
mentionCandidateNameTextView.text = candidate.displayName
|
||||
profilePictureView.root.publicKey = candidate.publicKey
|
||||
profilePictureView.root.displayName = candidate.displayName
|
||||
profilePictureView.root.additionalPublicKey = null
|
||||
profilePictureView.root.glide = glide!!
|
||||
profilePictureView.root.update()
|
||||
profilePictureView.publicKey = candidate.publicKey
|
||||
profilePictureView.displayName = candidate.displayName
|
||||
profilePictureView.additionalPublicKey = null
|
||||
profilePictureView.update()
|
||||
if (openGroupServer != null && openGroupRoom != null) {
|
||||
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey)
|
||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||
|
|
|
@ -67,9 +67,11 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
||||
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
|
||||
// Message detail
|
||||
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
|
||||
menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
|
||||
// Resend
|
||||
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
||||
// Resync
|
||||
menu.findItem(R.id.menu_context_resync).isVisible = (selectedItems.size == 1 && firstMessage.isSyncFailed)
|
||||
// Save media
|
||||
menu.findItem(R.id.menu_context_save_attachment).isVisible = (selectedItems.size == 1
|
||||
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
|
||||
|
@ -90,6 +92,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||
R.id.menu_context_ban_and_delete_all -> delegate?.banAndDeleteAll(selectedItems)
|
||||
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
|
||||
R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems)
|
||||
R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
|
||||
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
|
||||
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
|
||||
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
|
||||
|
@ -113,6 +116,7 @@ interface ConversationActionModeCallbackDelegate {
|
|||
fun banAndDeleteAll(messages: Set<MessageRecord>)
|
||||
fun copyMessages(messages: Set<MessageRecord>)
|
||||
fun copySessionID(messages: Set<MessageRecord>)
|
||||
fun resyncMessage(messages: Set<MessageRecord>)
|
||||
fun resendMessage(messages: Set<MessageRecord>)
|
||||
fun showMessageDetail(messages: Set<MessageRecord>)
|
||||
fun saveAttachment(messages: Set<MessageRecord>)
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.menus
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.PorterDuff
|
||||
|
@ -15,7 +14,6 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.appcompat.widget.SearchView
|
||||
|
@ -34,7 +32,6 @@ import org.session.libsession.utilities.recipients.Recipient
|
|||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity
|
||||
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||
|
@ -45,6 +42,8 @@ import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
|
|||
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
|
||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.showMuteDialog
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import java.io.IOException
|
||||
|
||||
|
@ -64,17 +63,18 @@ object ConversationMenuHelper {
|
|||
// Base menu (options that should always be present)
|
||||
inflater.inflate(R.menu.menu_conversation, menu)
|
||||
// Expiring messages
|
||||
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient)) {
|
||||
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient) && !thread.isBlocked) {
|
||||
if (thread.expireMessages > 0) {
|
||||
inflater.inflate(R.menu.menu_conversation_expiration_on, menu)
|
||||
val item = menu.findItem(R.id.menu_expiring_messages)
|
||||
val actionView = item.actionView
|
||||
val iconView = actionView.findViewById<ImageView>(R.id.menu_badge_icon)
|
||||
val badgeView = actionView.findViewById<TextView>(R.id.expiration_badge)
|
||||
@ColorInt val color = context.getColorFromAttr(android.R.attr.textColorPrimary)
|
||||
iconView.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)
|
||||
badgeView.text = ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, thread.expireMessages)
|
||||
actionView.setOnClickListener { onOptionsItemSelected(item) }
|
||||
item.actionView?.let { actionView ->
|
||||
val iconView = actionView.findViewById<ImageView>(R.id.menu_badge_icon)
|
||||
val badgeView = actionView.findViewById<TextView>(R.id.expiration_badge)
|
||||
@ColorInt val color = context.getColorFromAttr(android.R.attr.textColorPrimary)
|
||||
iconView.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)
|
||||
badgeView.text = ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, thread.expireMessages)
|
||||
actionView.setOnClickListener { onOptionsItemSelected(item) }
|
||||
}
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_conversation_expiration_off, menu)
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ object ConversationMenuHelper {
|
|||
if (thread.isContactRecipient) {
|
||||
if (thread.isBlocked) {
|
||||
inflater.inflate(R.menu.menu_conversation_unblock, menu)
|
||||
} else {
|
||||
} else if (!thread.isLocalNumber) {
|
||||
inflater.inflate(R.menu.menu_conversation_block, menu)
|
||||
}
|
||||
}
|
||||
|
@ -186,29 +186,23 @@ object ConversationMenuHelper {
|
|||
private fun call(context: Context, thread: Recipient) {
|
||||
|
||||
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.ConversationActivity_call_title)
|
||||
.setMessage(R.string.ConversationActivity_call_prompt)
|
||||
.setPositiveButton(R.string.activity_settings_title) { _, _ ->
|
||||
val intent = Intent(context, PrivacySettingsActivity::class.java)
|
||||
context.startActivity(intent)
|
||||
context.showSessionDialog {
|
||||
title(R.string.ConversationActivity_call_title)
|
||||
text(R.string.ConversationActivity_call_prompt)
|
||||
button(R.string.activity_settings_title, R.string.AccessibilityId_settings) {
|
||||
Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { d, _ ->
|
||||
d.dismiss()
|
||||
}.create()
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE)?.contentDescription = context.getString(R.string.AccessibilityId_settings)
|
||||
dialog.getButton(DialogInterface.BUTTON_NEGATIVE)?.contentDescription = context.getString(R.string.AccessibilityId_cancel_button)
|
||||
dialog.show()
|
||||
cancelButton()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val service = WebRtcCallService.createCall(context, thread)
|
||||
context.startService(service)
|
||||
WebRtcCallService.createCall(context, thread)
|
||||
.let(context::startService)
|
||||
|
||||
val activity = Intent(context, WebRtcCallActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(activity)
|
||||
Intent(context, WebRtcCallActivity::class.java)
|
||||
.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||
.let(context::startActivity)
|
||||
|
||||
}
|
||||
|
||||
|
@ -295,9 +289,7 @@ object ConversationMenuHelper {
|
|||
|
||||
private fun leaveClosedGroup(context: Context, thread: Recipient) {
|
||||
if (!thread.isClosedGroupRecipient) { return }
|
||||
val builder = AlertDialog.Builder(context)
|
||||
builder.setTitle(context.resources.getString(R.string.ConversationActivity_leave_group))
|
||||
builder.setCancelable(true)
|
||||
|
||||
val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
|
||||
val admins = group.admins
|
||||
val sessionID = TextSecurePreferences.getLocalNumber(context)
|
||||
|
@ -307,29 +299,25 @@ object ConversationMenuHelper {
|
|||
} else {
|
||||
context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)
|
||||
}
|
||||
builder.setMessage(message)
|
||||
builder.setPositiveButton(R.string.yes) { _, _ ->
|
||||
var groupPublicKey: String?
|
||||
var isClosedGroup: Boolean
|
||||
try {
|
||||
groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
|
||||
isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
|
||||
} catch (e: IOException) {
|
||||
groupPublicKey = null
|
||||
isClosedGroup = false
|
||||
}
|
||||
try {
|
||||
if (isClosedGroup) {
|
||||
MessageSender.leave(groupPublicKey!!, true)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
|
||||
|
||||
fun onLeaveFailed() = Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
|
||||
|
||||
context.showSessionDialog {
|
||||
title(R.string.ConversationActivity_leave_group)
|
||||
text(message)
|
||||
button(R.string.yes) {
|
||||
try {
|
||||
val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
|
||||
val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
|
||||
|
||||
if (isClosedGroup) MessageSender.leave(groupPublicKey, notifyUser = false)
|
||||
else onLeaveFailed()
|
||||
} catch (e: Exception) {
|
||||
onLeaveFailed()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
button(R.string.no)
|
||||
}
|
||||
builder.setNegativeButton(R.string.no, null)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun inviteContacts(context: Context, thread: Recipient) {
|
||||
|
@ -344,7 +332,7 @@ object ConversationMenuHelper {
|
|||
}
|
||||
|
||||
private fun mute(context: Context, thread: Recipient) {
|
||||
MuteDialog.show(ContextThemeWrapper(context, context.theme)) { until: Long ->
|
||||
showMuteDialog(ContextThemeWrapper(context, context.theme)) { until ->
|
||||
DatabaseComponent.get(context).recipientDatabase().setMuted(thread, until)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,25 +3,21 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Spannable
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.util.Linkify
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||
|
@ -29,6 +25,7 @@ import okhttp3.HttpUrl
|
|||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
|
@ -39,15 +36,16 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getInt
|
|||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.SearchUtil
|
||||
import org.thoughtcrime.securesms.util.getAccentColor
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VisibleMessageContentView : ConstraintLayout {
|
||||
private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) }
|
||||
var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
|
||||
var onContentDoubleTap: (() -> Unit)? = null
|
||||
var delegate: VisibleMessageViewDelegate? = null
|
||||
var indexInAdapter: Int = -1
|
||||
|
@ -61,21 +59,20 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||
// region Updating
|
||||
fun bind(
|
||||
message: MessageRecord,
|
||||
isStartOfMessageCluster: Boolean,
|
||||
isEndOfMessageCluster: Boolean,
|
||||
glide: GlideRequests,
|
||||
isStartOfMessageCluster: Boolean = true,
|
||||
isEndOfMessageCluster: Boolean = true,
|
||||
glide: GlideRequests = GlideApp.with(this),
|
||||
thread: Recipient,
|
||||
searchQuery: String?,
|
||||
contactIsTrusted: Boolean,
|
||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
||||
searchQuery: String? = null,
|
||||
contactIsTrusted: Boolean = true,
|
||||
onAttachmentNeedsDownload: (Long, Long) -> Unit,
|
||||
suppressThumbnails: Boolean = false
|
||||
) {
|
||||
// Background
|
||||
val background = getBackground(message.isOutgoing)
|
||||
val color = if (message.isOutgoing) context.getAccentColor()
|
||||
else context.getColorFromAttr(R.attr.message_received_background_color)
|
||||
val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
|
||||
background.colorFilter = filter
|
||||
binding.contentParent.background = background
|
||||
binding.contentParent.mainColor = color
|
||||
binding.contentParent.cornerRadius = resources.getDimension(R.dimen.message_corner_radius)
|
||||
|
||||
val onlyBodyMessage = message is SmsMessageRecord
|
||||
val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
|
||||
|
@ -132,7 +129,6 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||
delegate?.scrollToMessageIfPossible(quote.id)
|
||||
}
|
||||
}
|
||||
val hasMedia = message.slideDeck.asAttachments().isNotEmpty()
|
||||
}
|
||||
|
||||
if (message is MmsMessageRecord) {
|
||||
|
@ -189,7 +185,7 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
}
|
||||
message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty() -> {
|
||||
message is MmsMessageRecord && !suppressThumbnails && message.slideDeck.asAttachments().isNotEmpty() -> {
|
||||
/*
|
||||
* Images / Video attachment
|
||||
*/
|
||||
|
@ -223,6 +219,7 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||
}
|
||||
|
||||
binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody
|
||||
binding.contentParent.apply { isVisible = children.any { it.isVisible } }
|
||||
|
||||
if (message.body.isNotEmpty() && !hideBody) {
|
||||
val color = getTextColor(context, message)
|
||||
|
@ -241,14 +238,15 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||
binding.contentParent.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
private val onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
|
||||
|
||||
fun onContentClick(event: MotionEvent) {
|
||||
onContentClick.forEach { clickHandler -> clickHandler.invoke(event) }
|
||||
}
|
||||
|
||||
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
||||
listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
||||
|
||||
private fun getBackground(isOutgoing: Boolean): Drawable {
|
||||
val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
||||
return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!!
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
arrayOf(
|
||||
binding.deletedMessageView.root,
|
||||
|
@ -266,6 +264,15 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||
fun playVoiceMessage() {
|
||||
binding.voiceMessageView.root.togglePlayback()
|
||||
}
|
||||
|
||||
fun playHighlight() {
|
||||
// Show the highlight colour immediately then slowly fade out
|
||||
val targetColor = if (ThemeUtil.isDarkTheme(context)) context.getAccentColor() else resources.getColor(R.color.black, context.theme)
|
||||
val clearTargetColor = ColorUtils.setAlphaComponent(targetColor, 0)
|
||||
binding.contentParent.numShadowRenders = if (ThemeUtil.isDarkTheme(context)) 3 else 1
|
||||
binding.contentParent.sessionShadowColor = targetColor
|
||||
GlowViewUtilities.animateShadowColorChange(binding.contentParent, targetColor, clearTargetColor, 1600)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
|
|
|
@ -2,16 +2,17 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
|
@ -46,6 +47,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase
|
|||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
|
@ -70,7 +72,6 @@ class VisibleMessageView : LinearLayout {
|
|||
@Inject lateinit var mmsDb: MmsDatabase
|
||||
|
||||
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||
private val swipeToReplyIconRect = Rect()
|
||||
private var dx = 0.0f
|
||||
|
@ -111,7 +112,10 @@ class VisibleMessageView : LinearLayout {
|
|||
private fun initialize() {
|
||||
isHapticFeedbackEnabled = true
|
||||
setWillNotDraw(false)
|
||||
binding.root.disableClipping()
|
||||
binding.mainContainer.disableClipping()
|
||||
binding.messageInnerContainer.disableClipping()
|
||||
binding.messageInnerLayout.disableClipping()
|
||||
binding.messageContentView.root.disableClipping()
|
||||
}
|
||||
// endregion
|
||||
|
@ -119,13 +123,14 @@ class VisibleMessageView : LinearLayout {
|
|||
// region Updating
|
||||
fun bind(
|
||||
message: MessageRecord,
|
||||
previous: MessageRecord?,
|
||||
next: MessageRecord?,
|
||||
glide: GlideRequests,
|
||||
searchQuery: String?,
|
||||
contact: Contact?,
|
||||
previous: MessageRecord? = null,
|
||||
next: MessageRecord? = null,
|
||||
glide: GlideRequests = GlideApp.with(this),
|
||||
searchQuery: String? = null,
|
||||
contact: Contact? = null,
|
||||
senderSessionID: String,
|
||||
delegate: VisibleMessageViewDelegate?,
|
||||
lastSeen: Long,
|
||||
delegate: VisibleMessageViewDelegate? = null,
|
||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
||||
) {
|
||||
val threadID = message.threadId
|
||||
|
@ -136,7 +141,7 @@ class VisibleMessageView : LinearLayout {
|
|||
// Show profile picture and sender name if this is a group thread AND
|
||||
// the message is incoming
|
||||
binding.moderatorIconImageView.isVisible = false
|
||||
binding.profilePictureView.root.visibility = when {
|
||||
binding.profilePictureView.visibility = when {
|
||||
thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE
|
||||
thread.isGroupRecipient -> View.INVISIBLE
|
||||
else -> View.GONE
|
||||
|
@ -145,25 +150,25 @@ class VisibleMessageView : LinearLayout {
|
|||
val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||
else ViewUtil.dpToPx(context,2)
|
||||
|
||||
if (binding.profilePictureView.root.visibility == View.GONE) {
|
||||
if (binding.profilePictureView.visibility == View.GONE) {
|
||||
val expirationParams = binding.messageInnerContainer.layoutParams as MarginLayoutParams
|
||||
expirationParams.bottomMargin = bottomMargin
|
||||
binding.messageInnerContainer.layoutParams = expirationParams
|
||||
} else {
|
||||
val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams
|
||||
val avatarLayoutParams = binding.profilePictureView.layoutParams as MarginLayoutParams
|
||||
avatarLayoutParams.bottomMargin = bottomMargin
|
||||
binding.profilePictureView.root.layoutParams = avatarLayoutParams
|
||||
binding.profilePictureView.layoutParams = avatarLayoutParams
|
||||
}
|
||||
|
||||
if (isGroupThread && !message.isOutgoing) {
|
||||
if (isEndOfMessageCluster) {
|
||||
binding.profilePictureView.root.publicKey = senderSessionID
|
||||
binding.profilePictureView.root.glide = glide
|
||||
binding.profilePictureView.root.update(message.individualRecipient)
|
||||
binding.profilePictureView.root.setOnClickListener {
|
||||
binding.profilePictureView.publicKey = senderSessionID
|
||||
binding.profilePictureView.update(message.individualRecipient)
|
||||
binding.profilePictureView.setOnClickListener {
|
||||
if (thread.isOpenGroupRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
||||
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
||||
// TODO: support v2 soon
|
||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID))
|
||||
|
@ -177,7 +182,7 @@ class VisibleMessageView : LinearLayout {
|
|||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||
var standardPublicKey = ""
|
||||
var blindedPublicKey: String? = null
|
||||
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) {
|
||||
if (IdPrefix.fromValue(senderSessionID)?.isBlinded() == true) {
|
||||
blindedPublicKey = senderSessionID
|
||||
} else {
|
||||
standardPublicKey = senderSessionID
|
||||
|
@ -191,6 +196,8 @@ class VisibleMessageView : LinearLayout {
|
|||
val contactContext =
|
||||
if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
|
||||
// Unread marker
|
||||
binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
|
||||
// Date break
|
||||
val showDateBreak = isStartOfMessageCluster || snIsSelected
|
||||
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
||||
|
@ -292,50 +299,58 @@ class VisibleMessageView : LinearLayout {
|
|||
@StringRes val messageText: Int?,
|
||||
val contentDescription: String?)
|
||||
|
||||
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo {
|
||||
return when {
|
||||
!message.isOutgoing -> MessageStatusInfo(null,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
message.isFailed ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_failed,
|
||||
resources.getColor(R.color.destructive, context.theme),
|
||||
R.string.delivery_status_failed,
|
||||
null
|
||||
)
|
||||
message.isPending ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sending,
|
||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending,
|
||||
context.getString(R.string.AccessibilityId_message_sent_status_pending)
|
||||
)
|
||||
message.isRead ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_read,
|
||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read,
|
||||
null
|
||||
)
|
||||
else ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sent,
|
||||
context.getColorFromAttr(R.attr.message_status_color),
|
||||
R.string.delivery_status_sent,
|
||||
context.getString(R.string.AccessibilityId_message_sent_status_tick)
|
||||
)
|
||||
}
|
||||
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when {
|
||||
message.isFailed ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_failed,
|
||||
resources.getColor(R.color.destructive, context.theme),
|
||||
R.string.delivery_status_failed,
|
||||
null
|
||||
)
|
||||
message.isSyncFailed ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_failed,
|
||||
context.getColor(R.color.accent_orange),
|
||||
R.string.delivery_status_sync_failed,
|
||||
null
|
||||
)
|
||||
message.isPending ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sending,
|
||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending,
|
||||
context.getString(R.string.AccessibilityId_message_sent_status_pending)
|
||||
)
|
||||
message.isResyncing ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sending,
|
||||
context.getColor(R.color.accent_orange), R.string.delivery_status_syncing,
|
||||
context.getString(R.string.AccessibilityId_message_sent_status_syncing)
|
||||
)
|
||||
message.isRead ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_read,
|
||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read,
|
||||
null
|
||||
)
|
||||
else ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sent,
|
||||
context.getColorFromAttr(R.attr.message_status_color),
|
||||
R.string.delivery_status_sent,
|
||||
context.getString(R.string.AccessibilityId_message_sent_status_tick)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateExpirationTimer(message: MessageRecord) {
|
||||
val container = binding.messageInnerContainer
|
||||
val content = binding.messageContentView.root
|
||||
val expiration = binding.expirationTimerView
|
||||
val spacing = binding.messageContentSpacing
|
||||
container.removeAllViewsInLayout()
|
||||
container.addView(if (message.isOutgoing) expiration else content)
|
||||
container.addView(if (message.isOutgoing) content else expiration)
|
||||
container.addView(spacing, if (message.isOutgoing) 0 else 2)
|
||||
val layout = binding.messageInnerLayout
|
||||
|
||||
if (message.isOutgoing) binding.messageContentView.root.bringToFront()
|
||||
else binding.expirationTimerView.bringToFront()
|
||||
|
||||
layout.layoutParams = layout.layoutParams.let { it as FrameLayout.LayoutParams }
|
||||
.apply { gravity = if (message.isOutgoing) Gravity.END else Gravity.START }
|
||||
|
||||
val containerParams = container.layoutParams as ConstraintLayout.LayoutParams
|
||||
containerParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||
container.layoutParams = containerParams
|
||||
|
@ -381,7 +396,7 @@ class VisibleMessageView : LinearLayout {
|
|||
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||
val iconSize = toPx(24, context.resources)
|
||||
val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing
|
||||
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2)
|
||||
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.marginBottom - (iconSize / 2)
|
||||
val right = left + iconSize
|
||||
val bottom = top + iconSize
|
||||
swipeToReplyIconRect.left = left
|
||||
|
@ -401,9 +416,13 @@ class VisibleMessageView : LinearLayout {
|
|||
}
|
||||
|
||||
fun recycle() {
|
||||
binding.profilePictureView.root.recycle()
|
||||
binding.profilePictureView.recycle()
|
||||
binding.messageContentView.root.recycle()
|
||||
}
|
||||
|
||||
fun playHighlight() {
|
||||
binding.messageContentView.root.playHighlight()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
|
@ -498,7 +517,7 @@ class VisibleMessageView : LinearLayout {
|
|||
}
|
||||
|
||||
fun onContentClick(event: MotionEvent) {
|
||||
binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
|
||||
binding.messageContentView.root.onContentClick(event)
|
||||
}
|
||||
|
||||
private fun onPress(event: MotionEvent) {
|
||||
|
|
|
@ -92,7 +92,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
|
|||
if (progress == 1.0) {
|
||||
togglePlayback()
|
||||
handleProgressChanged(0.0)
|
||||
delegate?.playVoiceMessageAtIndexIfPossible(indexInAdapter - 1)
|
||||
delegate?.playVoiceMessageAtIndexIfPossible(indexInAdapter + 1)
|
||||
} else {
|
||||
handleProgressChanged(progress)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.content.Intent;
|
|||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
@ -244,12 +245,17 @@ public class AttachmentManager {
|
|||
}
|
||||
|
||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
|
||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||
.execute();
|
||||
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
||||
.request(Manifest.permission.READ_MEDIA_IMAGES);
|
||||
} else {
|
||||
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
}
|
||||
builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
|
||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectAudio(Activity activity, int requestCode) {
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
|
||||
open class BaseDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
setContentView(builder)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(requireContext())
|
||||
result.window?.setDimAmount(if (isLightMode) 0.1f else 0.75f)
|
||||
return result
|
||||
}
|
||||
|
||||
open fun setContentView(builder: AlertDialog.Builder) {
|
||||
// To be overridden by subclasses
|
||||
}
|
||||
}
|
|
@ -1,21 +1,18 @@
|
|||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
|
||||
object NotificationUtils {
|
||||
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
|
||||
val notifyTypes = context.resources.getStringArray(R.array.notify_types)
|
||||
val currentSelected = thread.notifyType
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setSingleChoiceItems(notifyTypes,currentSelected) { d, newSelection ->
|
||||
notifyTypeHandler(newSelection)
|
||||
d.dismiss()
|
||||
}
|
||||
.setTitle(R.string.RecipientPreferenceActivity_notification_settings)
|
||||
.show()
|
||||
context.showSessionDialog {
|
||||
title(R.string.RecipientPreferenceActivity_notification_settings)
|
||||
singleChoiceItems(
|
||||
context.resources.getStringArray(R.array.notify_types),
|
||||
thread.notifyType
|
||||
) { notifyTypeHandler(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
|
|||
|
||||
import android.content.Context
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.visible.LinkPreview
|
||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
||||
import org.session.libsession.messaging.messages.visible.Quote
|
||||
|
@ -15,7 +16,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
|||
|
||||
object ResendMessageUtilities {
|
||||
|
||||
fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?) {
|
||||
fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) {
|
||||
val recipient: Recipient = messageRecord.recipient
|
||||
val message = VisibleMessage()
|
||||
message.id = messageRecord.getId()
|
||||
|
@ -55,8 +56,13 @@ object ResendMessageUtilities {
|
|||
val sentTimestamp = message.sentTimestamp
|
||||
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
|
||||
if (sentTimestamp != null && sender != null) {
|
||||
MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender)
|
||||
if (isResync) {
|
||||
MessagingModuleConfiguration.shared.storage.markAsResyncing(sentTimestamp, sender)
|
||||
MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = true)
|
||||
} else {
|
||||
MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender)
|
||||
MessageSender.send(message, recipient.address)
|
||||
}
|
||||
}
|
||||
MessageSender.send(message, recipient.address)
|
||||
}
|
||||
}
|
|
@ -38,13 +38,12 @@ object TextUtilities {
|
|||
fun TextView.getIntersectedModalSpans(hitRect: Rect): List<ModalURLSpan> {
|
||||
val textLayout = layout ?: return emptyList()
|
||||
val lineRect = Rect()
|
||||
val bodyTextRect = Rect()
|
||||
getGlobalVisibleRect(bodyTextRect)
|
||||
val offset = intArrayOf(0, 0).also { getLocationOnScreen(it) }
|
||||
val textSpannable = text.toSpannable()
|
||||
return (0 until textLayout.lineCount).flatMap { line ->
|
||||
textLayout.getLineBounds(line, lineRect)
|
||||
lineRect.offset(bodyTextRect.left + totalPaddingLeft, bodyTextRect.top + totalPaddingTop)
|
||||
if ((Rect(lineRect)).contains(hitRect)) {
|
||||
lineRect.offset(offset[0] + totalPaddingLeft, offset[1] + totalPaddingTop)
|
||||
if (lineRect.contains(hitRect)) {
|
||||
// calculate the url span intersected with (if any)
|
||||
val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same
|
||||
textSpannable.getSpans<ModalURLSpan>(off, off).toList()
|
||||
|
|
|
@ -30,8 +30,7 @@ class ThumbnailProgressBar: View {
|
|||
private val objectRect = Rect()
|
||||
private val drawingRect = Rect()
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas?) {
|
||||
if (canvas == null) return
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
|
||||
getDrawingRect(objectRect)
|
||||
drawingRect.set(objectRect)
|
||||
|
|
|
@ -126,10 +126,9 @@ open class ThumbnailView: FrameLayout {
|
|||
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, result))
|
||||
}
|
||||
slide.hasPlaceholder() -> {
|
||||
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, result))
|
||||
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, null, result))
|
||||
}
|
||||
else -> {
|
||||
binding.thumbnailLoadIndicator.isVisible = false
|
||||
glide.clear(binding.thumbnailImage)
|
||||
result.set(false)
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ public class IdentityKeyUtil {
|
|||
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
|
||||
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
|
||||
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
|
||||
public static final String NOTIFICATION_KEY = "pref_notification_key";
|
||||
public static final String LOKI_SEED = "loki_seed";
|
||||
public static final String HAS_MIGRATED_KEY = "has_migrated_keys";
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.os.Build;
|
||||
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
@ -45,44 +45,50 @@ public final class KeyStoreHelper {
|
|||
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
||||
private static final String KEY_ALIAS = "SignalSecret";
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
public static SealedData seal(@NonNull byte[] input) {
|
||||
SecretKey secretKey = getOrCreateKeyStoreEntry();
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
|
||||
// prevent crashes with quickly repeated encrypt/decrypt operations
|
||||
// https://github.com/mozilla-mobile/android-components/issues/5342
|
||||
synchronized (CIPHER_LOCK) {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
|
||||
byte[] iv = cipher.getIV();
|
||||
byte[] data = cipher.doFinal(input);
|
||||
byte[] iv = cipher.getIV();
|
||||
byte[] data = cipher.doFinal(input);
|
||||
|
||||
return new SealedData(iv, data);
|
||||
return new SealedData(iv, data);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
public static byte[] unseal(@NonNull SealedData sealedData) {
|
||||
SecretKey secretKey = getKeyStoreEntry();
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
|
||||
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
|
||||
// prevent crashes with quickly repeated encrypt/decrypt operations
|
||||
// https://github.com/mozilla-mobile/android-components/issues/5342
|
||||
synchronized (CIPHER_LOCK) {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
|
||||
|
||||
return cipher.doFinal(sealedData.data);
|
||||
return cipher.doFinal(sealedData.data);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey getOrCreateKeyStoreEntry() {
|
||||
if (hasKeyStoreEntry()) return getKeyStoreEntry();
|
||||
else return createKeyStoreEntry();
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey createKeyStoreEntry() {
|
||||
try {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
|
||||
|
@ -99,7 +105,6 @@ public final class KeyStoreHelper {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey getKeyStoreEntry() {
|
||||
KeyStore keyStore = getKeyStore();
|
||||
|
||||
|
@ -137,7 +142,6 @@ public final class KeyStoreHelper {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static boolean hasKeyStoreEntry() {
|
||||
try {
|
||||
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
|
||||
|
@ -202,7 +206,5 @@ public final class KeyStoreHelper {
|
|||
return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.core.database.getBlobOrNull
|
||||
import androidx.core.database.getLongOrNull
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
|
||||
class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) {
|
||||
|
||||
companion object {
|
||||
private const val VARIANT = "variant"
|
||||
private const val PUBKEY = "publicKey"
|
||||
private const val DATA = "data"
|
||||
private const val TIMESTAMP = "timestamp" // Milliseconds
|
||||
|
||||
private const val TABLE_NAME = "configs_table"
|
||||
|
||||
const val CREATE_CONFIG_TABLE_COMMAND =
|
||||
"CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $TIMESTAMP INTEGER NOT NULL DEFAULT 0, PRIMARY KEY($VARIANT, $PUBKEY));"
|
||||
|
||||
private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
|
||||
}
|
||||
|
||||
fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) {
|
||||
val db = writableDatabase
|
||||
val contentValues = contentValuesOf(
|
||||
VARIANT to variant,
|
||||
PUBKEY to publicKey,
|
||||
DATA to data,
|
||||
TIMESTAMP to timestamp
|
||||
)
|
||||
db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey))
|
||||
}
|
||||
|
||||
fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? {
|
||||
val db = readableDatabase
|
||||
val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
|
||||
return query?.use { cursor ->
|
||||
if (!cursor.moveToFirst()) return@use null
|
||||
val bytes = cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) ?: return@use null
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveConfigLastUpdateTimestamp(variant: String, publicKey: String): Long {
|
||||
val db = readableDatabase
|
||||
val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
|
||||
if (cursor == null) return 0
|
||||
if (!cursor.moveToFirst()) return 0
|
||||
return (cursor.getLongOrNull(cursor.getColumnIndex(TIMESTAMP)) ?: 0)
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class FastCursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder, T>
|
||||
extends CursorRecyclerViewAdapter<VH>
|
||||
{
|
||||
private static final String TAG = FastCursorRecyclerViewAdapter.class.getSimpleName();
|
||||
|
||||
private final LinkedList<T> fastRecords = new LinkedList<>();
|
||||
private final List<Long> releasedRecordIds = new LinkedList<>();
|
||||
|
||||
protected FastCursorRecyclerViewAdapter(Context context, Cursor cursor) {
|
||||
super(context, cursor);
|
||||
}
|
||||
|
||||
public void addFastRecord(@NonNull T record) {
|
||||
fastRecords.addFirst(record);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void releaseFastRecord(long id) {
|
||||
synchronized (releasedRecordIds) {
|
||||
releasedRecordIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
protected void cleanFastRecords() {
|
||||
synchronized (releasedRecordIds) {
|
||||
Iterator<Long> releaseIdIterator = releasedRecordIds.iterator();
|
||||
|
||||
while (releaseIdIterator.hasNext()) {
|
||||
long releasedId = releaseIdIterator.next();
|
||||
Iterator<T> fastRecordIterator = fastRecords.iterator();
|
||||
|
||||
while (fastRecordIterator.hasNext()) {
|
||||
if (isRecordForId(fastRecordIterator.next(), releasedId)) {
|
||||
fastRecordIterator.remove();
|
||||
releaseIdIterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract T getRecordFromCursor(@NonNull Cursor cursor);
|
||||
protected abstract void onBindItemViewHolder(VH viewHolder, @NonNull T record);
|
||||
protected abstract long getItemId(@NonNull T record);
|
||||
protected abstract int getItemViewType(@NonNull T record);
|
||||
protected abstract boolean isRecordForId(@NonNull T record, long id);
|
||||
|
||||
@Override
|
||||
public int getItemViewType(@NonNull Cursor cursor) {
|
||||
T record = getRecordFromCursor(cursor);
|
||||
return getItemViewType(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor) {
|
||||
T record = getRecordFromCursor(cursor);
|
||||
onBindItemViewHolder(viewHolder, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindFastAccessItemViewHolder(VH viewHolder, int position) {
|
||||
int calculatedPosition = getCalculatedPosition(position);
|
||||
onBindItemViewHolder(viewHolder, fastRecords.get(calculatedPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getFastAccessSize() {
|
||||
return fastRecords.size();
|
||||
}
|
||||
|
||||
protected T getRecordForPositionOrThrow(int position) {
|
||||
if (isFastAccessPosition(position)) {
|
||||
return fastRecords.get(getCalculatedPosition(position));
|
||||
} else {
|
||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
||||
return getRecordFromCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
protected int getFastAccessItemViewType(int position) {
|
||||
return getItemViewType(fastRecords.get(getCalculatedPosition(position)));
|
||||
}
|
||||
|
||||
protected boolean isFastAccessPosition(int position) {
|
||||
position = getCalculatedPosition(position);
|
||||
return position >= 0 && position < fastRecords.size();
|
||||
}
|
||||
|
||||
protected long getFastAccessItemId(int position) {
|
||||
return getItemId(fastRecords.get(getCalculatedPosition(position)));
|
||||
}
|
||||
|
||||
private int getCalculatedPosition(int position) {
|
||||
return hasHeaderView() ? position - 1 : position;
|
||||
}
|
||||
|
||||
}
|
|
@ -36,9 +36,9 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||
@SuppressWarnings("unused")
|
||||
private static final String TAG = GroupDatabase.class.getSimpleName();
|
||||
|
||||
static final String TABLE_NAME = "groups";
|
||||
public static final String TABLE_NAME = "groups";
|
||||
private static final String ID = "_id";
|
||||
static final String GROUP_ID = "group_id";
|
||||
public static final String GROUP_ID = "group_id";
|
||||
private static final String TITLE = "title";
|
||||
private static final String MEMBERS = "members";
|
||||
private static final String ZOMBIE_MEMBERS = "zombie_members";
|
||||
|
@ -133,12 +133,12 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public List<GroupRecord> getAllGroups() {
|
||||
public List<GroupRecord> getAllGroups(boolean includeInactive) {
|
||||
Reader reader = getGroups();
|
||||
GroupRecord record;
|
||||
List<GroupRecord> groups = new LinkedList<>();
|
||||
while ((record = reader.getNext()) != null) {
|
||||
if (record.isActive()) { groups.add(record); }
|
||||
if (record.isActive() || includeInactive) { groups.add(record); }
|
||||
}
|
||||
reader.close();
|
||||
return groups;
|
||||
|
|
|
@ -1,249 +0,0 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class JobDatabase extends Database {
|
||||
|
||||
public static final String[] CREATE_TABLE = new String[] { Jobs.CREATE_TABLE,
|
||||
Constraints.CREATE_TABLE,
|
||||
Dependencies.CREATE_TABLE };
|
||||
|
||||
public static final class Jobs {
|
||||
public static final String TABLE_NAME = "job_spec";
|
||||
private static final String ID = "_id";
|
||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||
private static final String FACTORY_KEY = "factory_key";
|
||||
private static final String QUEUE_KEY = "queue_key";
|
||||
private static final String CREATE_TIME = "create_time";
|
||||
private static final String NEXT_RUN_ATTEMPT_TIME = "next_run_attempt_time";
|
||||
private static final String RUN_ATTEMPT = "run_attempt";
|
||||
private static final String MAX_ATTEMPTS = "max_attempts";
|
||||
private static final String MAX_BACKOFF = "max_backoff";
|
||||
private static final String MAX_INSTANCES = "max_instances";
|
||||
private static final String LIFESPAN = "lifespan";
|
||||
private static final String SERIALIZED_DATA = "serialized_data";
|
||||
private static final String IS_RUNNING = "is_running";
|
||||
|
||||
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
JOB_SPEC_ID + " TEXT UNIQUE, " +
|
||||
FACTORY_KEY + " TEXT, " +
|
||||
QUEUE_KEY + " TEXT, " +
|
||||
CREATE_TIME + " INTEGER, " +
|
||||
NEXT_RUN_ATTEMPT_TIME + " INTEGER, " +
|
||||
RUN_ATTEMPT + " INTEGER, " +
|
||||
MAX_ATTEMPTS + " INTEGER, " +
|
||||
MAX_BACKOFF + " INTEGER, " +
|
||||
MAX_INSTANCES + " INTEGER, " +
|
||||
LIFESPAN + " INTEGER, " +
|
||||
SERIALIZED_DATA + " TEXT, " +
|
||||
IS_RUNNING + " INTEGER)";
|
||||
}
|
||||
|
||||
public static final class Constraints {
|
||||
public static final String TABLE_NAME = "constraint_spec";
|
||||
private static final String ID = "_id";
|
||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||
private static final String FACTORY_KEY = "factory_key";
|
||||
|
||||
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
JOB_SPEC_ID + " TEXT, " +
|
||||
FACTORY_KEY + " TEXT, " +
|
||||
"UNIQUE(" + JOB_SPEC_ID + ", " + FACTORY_KEY + "))";
|
||||
}
|
||||
|
||||
public static final class Dependencies {
|
||||
public static final String TABLE_NAME = "dependency_spec";
|
||||
private static final String ID = "_id";
|
||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||
private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id";
|
||||
|
||||
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
JOB_SPEC_ID + " TEXT, " +
|
||||
DEPENDS_ON_JOB_SPEC_ID + " TEXT, " +
|
||||
"UNIQUE(" + JOB_SPEC_ID + ", " + DEPENDS_ON_JOB_SPEC_ID + "))";
|
||||
}
|
||||
|
||||
|
||||
public JobDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (FullSpec fullSpec : fullSpecs) {
|
||||
insertJobSpec(db, fullSpec.getJobSpec());
|
||||
insertConstraintSpecs(db, fullSpec.getConstraintSpecs());
|
||||
insertDependencySpecs(db, fullSpec.getDependencySpecs());
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized @NonNull List<JobSpec> getAllJobSpecs() {
|
||||
List<JobSpec> jobs = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
jobs.add(jobSpecFromCursor(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.IS_RUNNING, isRunning ? 1 : 0);
|
||||
|
||||
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||
String[] args = new String[]{ id };
|
||||
|
||||
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
|
||||
}
|
||||
|
||||
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.IS_RUNNING, isRunning ? 1 : 0);
|
||||
contentValues.put(Jobs.RUN_ATTEMPT, runAttempt);
|
||||
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, nextRunAttemptTime);
|
||||
|
||||
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||
String[] args = new String[]{ id };
|
||||
|
||||
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
|
||||
}
|
||||
|
||||
public synchronized void updateAllJobsToBePending() {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.IS_RUNNING, 0);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null);
|
||||
}
|
||||
|
||||
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (String jobId : jobIds) {
|
||||
String[] arg = new String[]{jobId};
|
||||
|
||||
db.delete(Jobs.TABLE_NAME, Jobs.JOB_SPEC_ID + " = ?", arg);
|
||||
db.delete(Constraints.TABLE_NAME, Constraints.JOB_SPEC_ID + " = ?", arg);
|
||||
db.delete(Dependencies.TABLE_NAME, Dependencies.JOB_SPEC_ID + " = ?", arg);
|
||||
db.delete(Dependencies.TABLE_NAME, Dependencies.DEPENDS_ON_JOB_SPEC_ID + " = ?", arg);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized @NonNull List<ConstraintSpec> getAllConstraintSpecs() {
|
||||
List<ConstraintSpec> constraints = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
constraints.add(constraintSpecFromCursor(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
public synchronized @NonNull List<DependencySpec> getAllDependencySpecs() {
|
||||
List<DependencySpec> dependencies = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
dependencies.add(dependencySpecFromCursor(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private void insertJobSpec(@NonNull SQLiteDatabase db, @NonNull JobSpec job) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||
contentValues.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||
contentValues.put(Jobs.QUEUE_KEY, job.getQueueKey());
|
||||
contentValues.put(Jobs.CREATE_TIME, job.getCreateTime());
|
||||
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
|
||||
contentValues.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
|
||||
contentValues.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
|
||||
contentValues.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
|
||||
contentValues.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
|
||||
contentValues.put(Jobs.LIFESPAN, job.getLifespan());
|
||||
contentValues.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||
contentValues.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0);
|
||||
|
||||
db.insertWithOnConflict(Jobs.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
|
||||
private void insertConstraintSpecs(@NonNull SQLiteDatabase db, @NonNull List<ConstraintSpec> constraints) {
|
||||
for (ConstraintSpec constraintSpec : constraints) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId());
|
||||
contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey());
|
||||
db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertDependencySpecs(@NonNull SQLiteDatabase db, @NonNull List<DependencySpec> dependencies) {
|
||||
for (DependencySpec dependencySpec : dependencies) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId());
|
||||
contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId());
|
||||
db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull JobSpec jobSpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new JobSpec(cursor.getString(cursor.getColumnIndexOrThrow(Jobs.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.FACTORY_KEY)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.QUEUE_KEY)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.CREATE_TIME)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.NEXT_RUN_ATTEMPT_TIME)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.RUN_ATTEMPT)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_ATTEMPTS)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.MAX_BACKOFF)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.LIFESPAN)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1);
|
||||
}
|
||||
|
||||
private @NonNull ConstraintSpec constraintSpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new ConstraintSpec(cursor.getString(cursor.getColumnIndexOrThrow(Constraints.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY)));
|
||||
}
|
||||
|
||||
private @NonNull DependencySpec dependencySpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new DependencySpec(cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)));
|
||||
}
|
||||
}
|
|
@ -458,9 +458,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removingIdPrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
|
||||
}
|
||||
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val timestamp = Date().time.toString()
|
||||
val index = "$groupPublicKey-$timestamp"
|
||||
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removingIdPrefixIfNeeded()
|
||||
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()
|
||||
|
|
|
@ -4,11 +4,8 @@ import android.content.ContentValues
|
|||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
|
||||
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||
|
||||
|
@ -24,12 +21,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||
val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
|
||||
}
|
||||
|
||||
fun getThreadID(hexEncodedPublicKey: String): Long {
|
||||
val address = Address.fromSerialized(hexEncodedPublicKey)
|
||||
val recipient = Recipient.from(context, address, false)
|
||||
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
|
||||
}
|
||||
|
||||
fun getAllOpenGroups(): Map<Long, OpenGroup> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
var cursor: Cursor? = null
|
||||
|
@ -61,6 +52,13 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||
}
|
||||
}
|
||||
|
||||
fun getThreadId(openGroup: OpenGroup): Long? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(publicChatTable, "$publicChat = ?", arrayOf(JsonUtil.toJson(openGroup.toJson()))) { cursor ->
|
||||
cursor.getLong(threadID)
|
||||
}
|
||||
}
|
||||
|
||||
fun setOpenGroupChat(openGroup: OpenGroup, threadID: Long) {
|
||||
if (threadID < 0) {
|
||||
return
|
||||
|
|
|
@ -37,6 +37,13 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||
public abstract void markExpireStarted(long messageId, long startTime);
|
||||
|
||||
public abstract void markAsSent(long messageId, boolean secure);
|
||||
|
||||
public abstract void markAsSyncing(long id);
|
||||
|
||||
public abstract void markAsResyncing(long id);
|
||||
|
||||
public abstract void markAsSyncFailed(long id);
|
||||
|
||||
public abstract void markUnidentified(long messageId, boolean unidentified);
|
||||
|
||||
public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention);
|
||||
|
@ -199,7 +206,6 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||
contentValues.put(THREAD_ID, newThreadId);
|
||||
db.update(getTableName(), contentValues, where, args);
|
||||
}
|
||||
|
||||
public static class SyncMessageId {
|
||||
|
||||
private final Address address;
|
||||
|
|
|
@ -20,13 +20,11 @@ import android.content.ContentValues
|
|||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import com.annimon.stream.Stream
|
||||
import com.google.android.mms.pdu_alt.NotificationInd
|
||||
import com.google.android.mms.pdu_alt.PduHeaders
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage
|
||||
|
@ -41,16 +39,13 @@ import org.session.libsession.utilities.Address.Companion.UNKNOWN
|
|||
import org.session.libsession.utilities.Address.Companion.fromExternal
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.Contact
|
||||
import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch
|
||||
import org.session.libsession.utilities.IdentityKeyMismatchList
|
||||
import org.session.libsession.utilities.NetworkFailure
|
||||
import org.session.libsession.utilities.NetworkFailureList
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled
|
||||
import org.session.libsession.utilities.Util.toIsoBytes
|
||||
import org.session.libsession.utilities.Util.toIsoString
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.recipients.RecipientFormattingException
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.ThreadUtils.queue
|
||||
|
@ -60,11 +55,13 @@ import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener
|
|||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.util.asSequence
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.security.SecureRandom
|
||||
|
@ -91,54 +88,22 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
return 0
|
||||
}
|
||||
|
||||
fun addFailures(messageId: Long, failure: List<NetworkFailure>) {
|
||||
try {
|
||||
addToDocument(messageId, NETWORK_FAILURE, failure, NetworkFailureList::class.java)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean =
|
||||
databaseHelper.writableDatabase.query(
|
||||
TABLE_NAME,
|
||||
arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS),
|
||||
DATE_SENT + " = ?",
|
||||
arrayOf(timestamp.toString()),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use { cursor ->
|
||||
cursor.asSequence()
|
||||
.map { cursor.getColumnIndexOrThrow(MESSAGE_BOX) }
|
||||
.map(cursor::getLong)
|
||||
.any { MmsSmsColumns.Types.isOutgoingMessageType(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFailure(messageId: Long, failure: NetworkFailure?) {
|
||||
try {
|
||||
removeFromDocument(messageId, NETWORK_FAILURE, failure, NetworkFailureList::class.java)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean {
|
||||
val database = databaseHelper.writableDatabase
|
||||
var cursor: Cursor? = null
|
||||
var isOutgoing = false
|
||||
try {
|
||||
cursor = database.query(
|
||||
TABLE_NAME,
|
||||
arrayOf<String>(ID, THREAD_ID, MESSAGE_BOX, ADDRESS),
|
||||
DATE_SENT + " = ?",
|
||||
arrayOf(timestamp.toString()),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
while (cursor.moveToNext()) {
|
||||
if (MmsSmsColumns.Types.isOutgoingMessageType(
|
||||
cursor.getLong(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
MESSAGE_BOX
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
isOutgoing = true
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
return isOutgoing
|
||||
}
|
||||
|
||||
fun incrementReceiptCount(
|
||||
messageId: SyncMessageId,
|
||||
|
@ -192,7 +157,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
)
|
||||
get(context).groupReceiptDatabase()
|
||||
.update(ourAddress, id, status, timestamp)
|
||||
get(context).threadDatabase().update(threadId, false)
|
||||
get(context).threadDatabase().update(threadId, false, true)
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
}
|
||||
|
@ -235,34 +200,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(RecipientFormattingException::class, MmsException::class)
|
||||
private fun getThreadIdFor(retrieved: IncomingMediaMessage): Long {
|
||||
return if (retrieved.groupId != null) {
|
||||
val groupRecipients = Recipient.from(
|
||||
context,
|
||||
retrieved.groupId,
|
||||
true
|
||||
)
|
||||
get(context).threadDatabase().getOrCreateThreadIdFor(groupRecipients)
|
||||
} else {
|
||||
val sender = Recipient.from(
|
||||
context,
|
||||
retrieved.from,
|
||||
true
|
||||
)
|
||||
get(context).threadDatabase().getOrCreateThreadIdFor(sender)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThreadIdFor(notification: NotificationInd): Long {
|
||||
val fromString =
|
||||
if (notification.from != null && notification.from.textString != null) toIsoString(
|
||||
notification.from.textString
|
||||
) else ""
|
||||
val recipient = Recipient.from(context, fromExternal(context, fromString), false)
|
||||
return get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
|
||||
}
|
||||
|
||||
private fun rawQuery(where: String, arguments: Array<String>?): Cursor {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.rawQuery(
|
||||
|
@ -273,10 +210,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
)
|
||||
}
|
||||
|
||||
fun getMessages(idsAsString: String): Cursor {
|
||||
return rawQuery(idsAsString, null)
|
||||
}
|
||||
|
||||
fun getMessage(messageId: Long): Cursor {
|
||||
val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
|
||||
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId))
|
||||
|
@ -302,52 +235,44 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
" WHERE " + ID + " = ?", arrayOf(id.toString() + "")
|
||||
)
|
||||
if (threadId.isPresent) {
|
||||
get(context).threadDatabase().update(threadId.get(), false)
|
||||
get(context).threadDatabase().update(threadId.get(), false, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun markAsPendingInsecureSmsFallback(messageId: Long) {
|
||||
val threadId = getThreadIdForMessage(messageId)
|
||||
private fun markAs(
|
||||
messageId: Long,
|
||||
baseType: Long,
|
||||
threadId: Long = getThreadIdForMessage(messageId)
|
||||
) {
|
||||
updateMailboxBitmask(
|
||||
messageId,
|
||||
MmsSmsColumns.Types.BASE_TYPE_MASK,
|
||||
MmsSmsColumns.Types.BASE_PENDING_INSECURE_SMS_FALLBACK,
|
||||
baseType,
|
||||
Optional.of(threadId)
|
||||
)
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
|
||||
override fun markAsSyncing(messageId: Long) {
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_SYNCING_TYPE)
|
||||
}
|
||||
override fun markAsResyncing(messageId: Long) {
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_RESYNCING_TYPE)
|
||||
}
|
||||
override fun markAsSyncFailed(messageId: Long) {
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_SYNC_FAILED_TYPE)
|
||||
}
|
||||
|
||||
fun markAsSending(messageId: Long) {
|
||||
val threadId = getThreadIdForMessage(messageId)
|
||||
updateMailboxBitmask(
|
||||
messageId,
|
||||
MmsSmsColumns.Types.BASE_TYPE_MASK,
|
||||
MmsSmsColumns.Types.BASE_SENDING_TYPE,
|
||||
Optional.of(threadId)
|
||||
)
|
||||
notifyConversationListeners(threadId)
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_SENDING_TYPE)
|
||||
}
|
||||
|
||||
fun markAsSentFailed(messageId: Long) {
|
||||
val threadId = getThreadIdForMessage(messageId)
|
||||
updateMailboxBitmask(
|
||||
messageId,
|
||||
MmsSmsColumns.Types.BASE_TYPE_MASK,
|
||||
MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE,
|
||||
Optional.of(threadId)
|
||||
)
|
||||
notifyConversationListeners(threadId)
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE)
|
||||
}
|
||||
|
||||
override fun markAsSent(messageId: Long, secure: Boolean) {
|
||||
val threadId = getThreadIdForMessage(messageId)
|
||||
updateMailboxBitmask(
|
||||
messageId,
|
||||
MmsSmsColumns.Types.BASE_TYPE_MASK,
|
||||
MmsSmsColumns.Types.BASE_SENT_TYPE or if (secure) MmsSmsColumns.Types.PUSH_MESSAGE_BIT or MmsSmsColumns.Types.SECURE_MESSAGE_BIT else 0,
|
||||
Optional.of(threadId)
|
||||
)
|
||||
notifyConversationListeners(threadId)
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_SENT_TYPE or if (secure) MmsSmsColumns.Types.PUSH_MESSAGE_BIT or MmsSmsColumns.Types.SECURE_MESSAGE_BIT else 0)
|
||||
}
|
||||
|
||||
override fun markUnidentified(messageId: Long, unidentified: Boolean) {
|
||||
|
@ -367,17 +292,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
val attachmentDatabase = get(context).attachmentDatabase()
|
||||
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) })
|
||||
val threadId = getThreadIdForMessage(messageId)
|
||||
if (!read) {
|
||||
val mentionChange = if (hasMention) { 1 } else { 0 }
|
||||
get(context).threadDatabase().decrementUnread(threadId, 1, mentionChange)
|
||||
}
|
||||
updateMailboxBitmask(
|
||||
messageId,
|
||||
MmsSmsColumns.Types.BASE_TYPE_MASK,
|
||||
MmsSmsColumns.Types.BASE_DELETED_TYPE,
|
||||
Optional.of(threadId)
|
||||
)
|
||||
notifyConversationListeners(threadId)
|
||||
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_DELETED_TYPE, threadId)
|
||||
}
|
||||
|
||||
override fun markExpireStarted(messageId: Long) {
|
||||
|
@ -400,6 +316,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
fun setMessagesRead(threadId: Long, beforeTime: Long): List<MarkedMessageInfo> {
|
||||
return setMessagesRead(
|
||||
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?",
|
||||
arrayOf(threadId.toString(), beforeTime.toString())
|
||||
)
|
||||
}
|
||||
|
||||
fun setMessagesRead(threadId: Long): List<MarkedMessageInfo> {
|
||||
return setMessagesRead(
|
||||
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)",
|
||||
|
@ -407,10 +330,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
)
|
||||
}
|
||||
|
||||
fun setAllMessagesRead(): List<MarkedMessageInfo> {
|
||||
return setMessagesRead(READ + " = 0", null)
|
||||
}
|
||||
|
||||
private fun setMessagesRead(where: String, arguments: Array<String>?): List<MarkedMessageInfo> {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val result: MutableList<MarkedMessageInfo> = LinkedList()
|
||||
|
@ -419,7 +338,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
try {
|
||||
cursor = database.query(
|
||||
TABLE_NAME,
|
||||
arrayOf<String>(ID, ADDRESS, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED),
|
||||
arrayOf(ID, ADDRESS, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED),
|
||||
where,
|
||||
arguments,
|
||||
null,
|
||||
|
@ -628,18 +547,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
contentLocation: String,
|
||||
threadId: Long, mailbox: Long,
|
||||
serverTimestamp: Long,
|
||||
runIncrement: Boolean,
|
||||
runThreadUpdate: Boolean
|
||||
): Optional<InsertResult> {
|
||||
var threadId = threadId
|
||||
if (threadId == -1L || retrieved.isGroupMessage) {
|
||||
try {
|
||||
threadId = getThreadIdFor(retrieved)
|
||||
} catch (e: RecipientFormattingException) {
|
||||
Log.w("MmsDatabase", e)
|
||||
if (threadId == -1L) throw MmsException(e)
|
||||
}
|
||||
}
|
||||
if (threadId < 0 ) throw MmsException("No thread ID supplied!")
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(DATE_SENT, retrieved.sentTimeMillis)
|
||||
contentValues.put(ADDRESS, retrieved.from.serialize())
|
||||
|
@ -693,12 +603,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
null,
|
||||
)
|
||||
if (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) {
|
||||
if (runIncrement) {
|
||||
val mentionAmount = if (retrieved.hasMention()) { 1 } else { 0 }
|
||||
get(context).threadDatabase().incrementUnread(threadId, 1, mentionAmount)
|
||||
}
|
||||
if (runThreadUpdate) {
|
||||
get(context).threadDatabase().update(threadId, true)
|
||||
get(context).threadDatabase().update(threadId, true, true)
|
||||
}
|
||||
}
|
||||
notifyConversationListeners(threadId)
|
||||
|
@ -712,27 +618,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
serverTimestamp: Long,
|
||||
runThreadUpdate: Boolean
|
||||
): Optional<InsertResult> {
|
||||
var threadId = threadId
|
||||
if (threadId == -1L) {
|
||||
if (retrieved.isGroup) {
|
||||
val decodedGroupId: String = if (retrieved is OutgoingExpirationUpdateMessage) {
|
||||
retrieved.groupId
|
||||
} else {
|
||||
(retrieved as OutgoingGroupMediaMessage).groupId
|
||||
}
|
||||
val groupId: String
|
||||
groupId = try {
|
||||
doubleEncodeGroupID(decodedGroupId)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Couldn't encrypt group ID")
|
||||
throw MmsException(e)
|
||||
}
|
||||
val group = Recipient.from(context, fromSerialized(groupId), false)
|
||||
threadId = get(context).threadDatabase().getOrCreateThreadIdFor(group)
|
||||
} else {
|
||||
threadId = get(context).threadDatabase().getOrCreateThreadIdFor(retrieved.recipient)
|
||||
}
|
||||
}
|
||||
if (threadId < 0 ) throw MmsException("No thread ID supplied!")
|
||||
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
|
||||
if (messageId == -1L) {
|
||||
return Optional.absent()
|
||||
|
@ -747,7 +633,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
retrieved: IncomingMediaMessage,
|
||||
threadId: Long,
|
||||
serverTimestamp: Long = 0,
|
||||
runIncrement: Boolean,
|
||||
runThreadUpdate: Boolean
|
||||
): Optional<InsertResult> {
|
||||
var type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT
|
||||
|
@ -766,7 +651,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
if (retrieved.isMessageRequestResponse) {
|
||||
type = type or MmsSmsColumns.Types.MESSAGE_REQUEST_RESPONSE_BIT
|
||||
}
|
||||
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runIncrement, runThreadUpdate)
|
||||
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runThreadUpdate)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
|
@ -855,10 +740,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
)
|
||||
}
|
||||
with (get(context).threadDatabase()) {
|
||||
setLastSeen(threadId)
|
||||
val lastSeen = getLastSeenAndHasSent(threadId).first()
|
||||
if (lastSeen < message.sentTimeMillis) {
|
||||
setLastSeen(threadId, message.sentTimeMillis)
|
||||
}
|
||||
setHasSent(threadId, true)
|
||||
if (runThreadUpdate) {
|
||||
update(threadId, true)
|
||||
update(threadId, true, true)
|
||||
}
|
||||
}
|
||||
return messageId
|
||||
|
@ -993,7 +881,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
groupReceiptDatabase.deleteRowsForMessage(messageId)
|
||||
val database = databaseHelper.writableDatabase
|
||||
database!!.delete(TABLE_NAME, ID_WHERE, arrayOf(messageId.toString()))
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false)
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
||||
notifyConversationListeners(threadId)
|
||||
notifyStickerListeners()
|
||||
notifyStickerPackListeners()
|
||||
|
@ -1010,7 +898,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
val database = databaseHelper.writableDatabase
|
||||
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
|
||||
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false)
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
||||
notifyConversationListeners(threadId)
|
||||
notifyStickerListeners()
|
||||
notifyStickerPackListeners()
|
||||
|
@ -1208,7 +1096,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
}
|
||||
val threadDb = get(context).threadDatabase()
|
||||
for (threadId in threadIds) {
|
||||
val threadDeleted = threadDb.update(threadId, false)
|
||||
val threadDeleted = threadDb.update(threadId, false, true)
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
notifyStickerListeners()
|
||||
|
@ -1400,25 +1288,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
val attachments = get(context).attachmentDatabase().getAttachment(
|
||||
cursor
|
||||
)
|
||||
val contacts: List<Contact?> = getSharedContacts(
|
||||
cursor, attachments
|
||||
)
|
||||
val contactAttachments =
|
||||
contacts.map { obj: Contact? -> obj!!.avatarAttachment }
|
||||
.filter { a: Attachment? -> a != null }
|
||||
.toSet()
|
||||
val previews: List<LinkPreview?> = getLinkPreviews(
|
||||
cursor, attachments
|
||||
)
|
||||
val previewAttachments =
|
||||
previews.filter { lp: LinkPreview? -> lp!!.getThumbnail().isPresent }
|
||||
.map { lp: LinkPreview? -> lp!!.getThumbnail().get() }
|
||||
.toSet()
|
||||
val contacts: List<Contact?> = getSharedContacts(cursor, attachments)
|
||||
val contactAttachments: Set<Attachment?> =
|
||||
contacts.mapNotNull { it?.avatarAttachment }.toSet()
|
||||
val previews: List<LinkPreview?> = getLinkPreviews(cursor, attachments)
|
||||
val previewAttachments: Set<Attachment?> =
|
||||
previews.mapNotNull { it?.getThumbnail()?.orNull() }.toSet()
|
||||
val slideDeck = getSlideDeck(
|
||||
Stream.of(attachments)
|
||||
.filterNot { o: DatabaseAttachment? -> contactAttachments.contains(o) }
|
||||
.filterNot { o: DatabaseAttachment? -> previewAttachments.contains(o) }
|
||||
.toList()
|
||||
attachments
|
||||
.filterNot { o: DatabaseAttachment? -> o in contactAttachments }
|
||||
.filterNot { o: DatabaseAttachment? -> o in previewAttachments }
|
||||
)
|
||||
val quote = getQuote(cursor)
|
||||
val reactions = get(context).reactionDatabase().getReactions(cursor)
|
||||
|
@ -1476,11 +1355,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor)
|
||||
val quoteText = retrievedQuote?.body
|
||||
val quoteMissing = retrievedQuote == null
|
||||
val attachments = get(context).attachmentDatabase().getAttachment(cursor)
|
||||
val quoteAttachments: List<Attachment?>? =
|
||||
Stream.of(attachments).filter { obj: DatabaseAttachment? -> obj!!.isQuote }
|
||||
val quoteDeck = (
|
||||
(retrievedQuote as? MmsMessageRecord)?.slideDeck ?:
|
||||
Stream.of(get(context).attachmentDatabase().getAttachment(cursor))
|
||||
.filter { obj: DatabaseAttachment? -> obj!!.isQuote }
|
||||
.toList()
|
||||
val quoteDeck = SlideDeck(context, quoteAttachments!!)
|
||||
.let { SlideDeck(context, it) }
|
||||
)
|
||||
return Quote(
|
||||
quoteId,
|
||||
fromExternal(context, quoteAuthor),
|
||||
|
@ -1623,4 +1504,4 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||
const val CREATE_REACTIONS_LAST_SEEN_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $REACTIONS_LAST_SEEN INTEGER DEFAULT 0;"
|
||||
const val CREATE_HAS_MENTION_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $HAS_MENTION INTEGER DEFAULT 0;"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,8 +47,13 @@ public interface MmsSmsColumns {
|
|||
protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26;
|
||||
public static final long BASE_DRAFT_TYPE = 27;
|
||||
protected static final long BASE_DELETED_TYPE = 28;
|
||||
protected static final long BASE_SYNCING_TYPE = 29;
|
||||
protected static final long BASE_RESYNCING_TYPE = 30;
|
||||
protected static final long BASE_SYNC_FAILED_TYPE = 31;
|
||||
|
||||
protected static final long[] OUTGOING_MESSAGE_TYPES = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE,
|
||||
BASE_SYNCING_TYPE, BASE_RESYNCING_TYPE,
|
||||
BASE_SYNC_FAILED_TYPE,
|
||||
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
|
||||
BASE_PENDING_SECURE_SMS_FALLBACK,
|
||||
BASE_PENDING_INSECURE_SMS_FALLBACK,
|
||||
|
@ -109,6 +114,18 @@ public interface MmsSmsColumns {
|
|||
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isResyncingType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_RESYNCING_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isSyncingType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_SYNCING_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isSyncFailedMessageType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_SYNC_FAILED_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isFailedMessageType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_SENT_FAILED_TYPE;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.MmsDatabase.MESSAGE_BOX;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
|
@ -25,6 +27,7 @@ import androidx.annotation.Nullable;
|
|||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
|
@ -36,6 +39,8 @@ import java.io.Closeable;
|
|||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import kotlin.Pair;
|
||||
|
||||
public class MmsSmsDatabase extends Database {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
@ -259,8 +264,8 @@ public class MmsSmsDatabase extends Database {
|
|||
return -1;
|
||||
}
|
||||
|
||||
public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||
public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address, boolean reverse) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC");
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
||||
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) {
|
||||
|
@ -512,6 +517,23 @@ public class MmsSmsDatabase extends Database {
|
|||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Pair<Boolean, Long> timestampAndDirectionForCurrent(@NotNull Cursor cursor) {
|
||||
int sentColumn = cursor.getColumnIndex(MmsSmsColumns.NORMALIZED_DATE_SENT);
|
||||
String msgType = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT));
|
||||
long sentTime = cursor.getLong(sentColumn);
|
||||
long type = 0;
|
||||
if (MmsSmsDatabase.MMS_TRANSPORT.equals(msgType)) {
|
||||
int typeIndex = cursor.getColumnIndex(MESSAGE_BOX);
|
||||
type = cursor.getLong(typeIndex);
|
||||
} else if (MmsSmsDatabase.SMS_TRANSPORT.equals(msgType)) {
|
||||
int typeIndex = cursor.getColumnIndex(SmsDatabase.TYPE);
|
||||
type = cursor.getLong(typeIndex);
|
||||
}
|
||||
|
||||
return new Pair<Boolean, Long>(MmsSmsColumns.Types.isOutgoingMessageType(type), sentTime);
|
||||
}
|
||||
|
||||
public class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
|
|
@ -62,13 +62,15 @@ public class RecipientDatabase extends Database {
|
|||
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
|
||||
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
|
||||
private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none
|
||||
private static final String WRAPPER_HASH = "wrapper_hash";
|
||||
private static final String BLOCKS_COMMUNITY_MESSAGE_REQUESTS = "blocks_community_message_requests";
|
||||
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
|
||||
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
|
||||
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION, NOTIFY_TYPE,
|
||||
FORCE_SMS_SELECTION, NOTIFY_TYPE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS
|
||||
};
|
||||
|
||||
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
|
@ -136,6 +138,16 @@ public class RecipientDatabase extends Database {
|
|||
"OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))";
|
||||
}
|
||||
|
||||
public static String getAddWrapperHash() {
|
||||
return "ALTER TABLE "+TABLE_NAME+" "+
|
||||
"ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;";
|
||||
}
|
||||
|
||||
public static String getAddBlocksCommunityMessageRequests() {
|
||||
return "ALTER TABLE "+TABLE_NAME+" "+
|
||||
"ADD COLUMN "+BLOCKS_COMMUNITY_MESSAGE_REQUESTS+" INT DEFAULT 0;";
|
||||
}
|
||||
|
||||
public static final int NOTIFY_TYPE_ALL = 0;
|
||||
public static final int NOTIFY_TYPE_MENTIONS = 1;
|
||||
public static final int NOTIFY_TYPE_NONE = 2;
|
||||
|
@ -154,18 +166,14 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
public Optional<RecipientSettings> getRecipientSettings(@NonNull Address address) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[] {address.serialize()}, null, null, null);
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) {
|
||||
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return getRecipientSettings(cursor);
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,6 +202,8 @@ public class RecipientDatabase extends Database {
|
|||
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
|
||||
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
|
||||
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
|
||||
String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH));
|
||||
boolean blocksCommunityMessageRequests = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKS_COMMUNITY_MESSAGE_REQUESTS)) == 1;
|
||||
|
||||
MaterialColor color;
|
||||
byte[] profileKey = null;
|
||||
|
@ -225,7 +235,7 @@ public class RecipientDatabase extends Database {
|
|||
systemPhoneLabel, systemContactUri,
|
||||
signalProfileName, signalProfileAvatar, profileSharing,
|
||||
notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
forceSmsSelection));
|
||||
forceSmsSelection, wrapperHash, blocksCommunityMessageRequests));
|
||||
}
|
||||
|
||||
public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) {
|
||||
|
@ -252,6 +262,24 @@ public class RecipientDatabase extends Database {
|
|||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public boolean getApproved(@NonNull Address address) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{APPROVED}, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED)) == 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setRecipientHash(@NonNull Recipient recipient, String recipientHash) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(WRAPPER_HASH, recipientHash);
|
||||
updateOrInsert(recipient.getAddress(), values);
|
||||
recipient.resolve().setWrapperHash(recipientHash);
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setApproved(@NonNull Recipient recipient, boolean approved) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(APPROVED, approved ? 1 : 0);
|
||||
|
@ -268,15 +296,7 @@ public class RecipientDatabase extends Database {
|
|||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setBlocked(@NonNull Recipient recipient, boolean blocked) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(BLOCK, blocked ? 1 : 0);
|
||||
updateOrInsert(recipient.getAddress(), values);
|
||||
recipient.resolve().setBlocked(blocked);
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setBlocked(@NonNull List<Recipient> recipients, boolean blocked) {
|
||||
public void setBlocked(@NonNull Iterable<Recipient> recipients, boolean blocked) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
|
@ -382,6 +402,14 @@ public class RecipientDatabase extends Database {
|
|||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setBlocksCommunityMessageRequests(@NonNull Recipient recipient, boolean isBlocked) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(BLOCKS_COMMUNITY_MESSAGE_REQUESTS, isBlocked ? 1 : 0);
|
||||
updateOrInsert(recipient.getAddress(), contentValues);
|
||||
recipient.resolve().setBlocksCommunityMessageRequests(isBlocked);
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
private void updateOrInsert(Address address, ContentValues contentValues) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
|
|
|
@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.database
|
|||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import androidx.core.database.getStringOrNull
|
||||
import android.database.Cursor
|
||||
import androidx.core.database.getStringOrNull
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
|
||||
class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||
|
@ -43,6 +45,9 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(sessionContactTable, null, null) { cursor ->
|
||||
contactFromCursor(cursor)
|
||||
}.filter { contact ->
|
||||
val sessionId = SessionId(contact.sessionID)
|
||||
sessionId.prefix == IdPrefix.STANDARD
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
|
|||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||
|
@ -26,6 +27,9 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||
const val serializedData = "serialized_data"
|
||||
@JvmStatic val createSessionJobTableCommand
|
||||
= "CREATE TABLE $sessionJobTable ($jobID INTEGER PRIMARY KEY, $jobType STRING, $failureCount INTEGER DEFAULT 0, $serializedData TEXT);"
|
||||
|
||||
const val dropAttachmentDownloadJobs =
|
||||
"DELETE FROM $sessionJobTable WHERE $jobType = '${AttachmentDownloadJob.KEY}';"
|
||||
}
|
||||
|
||||
fun persistJob(job: Job) {
|
||||
|
@ -46,7 +50,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||
databaseHelper.writableDatabase.delete(sessionJobTable, "${Companion.jobID} = ?", arrayOf( jobID ))
|
||||
}
|
||||
|
||||
fun getAllPendingJobs(type: String): Map<String, Job?> {
|
||||
fun getAllJobs(type: String): Map<String, Job?> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf( type )) { cursor ->
|
||||
val jobID = cursor.getString(jobID)
|
||||
|
@ -93,6 +97,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||
fun cancelPendingMessageSendJobs(threadID: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val attachmentUploadJobKeys = mutableListOf<String>()
|
||||
database.beginTransaction()
|
||||
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor ->
|
||||
val job = jobFromCursor(cursor) as AttachmentUploadJob?
|
||||
if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) }
|
||||
|
@ -103,15 +108,19 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||
if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) }
|
||||
}
|
||||
if (attachmentUploadJobKeys.isNotEmpty()) {
|
||||
val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ")
|
||||
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
|
||||
arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString ))
|
||||
attachmentUploadJobKeys.forEach {
|
||||
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} = ?",
|
||||
arrayOf( AttachmentUploadJob.KEY, it ))
|
||||
}
|
||||
}
|
||||
if (messageSendJobKeys.isNotEmpty()) {
|
||||
val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ")
|
||||
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
|
||||
arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString ))
|
||||
messageSendJobKeys.forEach {
|
||||
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} = ?",
|
||||
arrayOf( MessageSendJob.KEY, it ))
|
||||
}
|
||||
}
|
||||
database.setTransactionSuccessful()
|
||||
database.endTransaction()
|
||||
}
|
||||
|
||||
fun isJobCanceled(job: Job): Boolean {
|
||||
|
|
|
@ -148,7 +148,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
|
||||
long threadId = getThreadIdForMessage(id);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
|
@ -202,6 +202,21 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsSyncing(long id) {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SYNCING_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsResyncing(long id) {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_RESYNCING_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsSyncFailed(long id) {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SYNC_FAILED_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markUnidentified(long id, boolean unidentified) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
|
@ -219,10 +234,6 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
contentValues.put(BODY, "");
|
||||
contentValues.put(HAS_MENTION, 0);
|
||||
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
if (!read) {
|
||||
DatabaseComponent.get(context).threadDatabase().decrementUnread(threadId, 1, (hasMention ? 1 : 0));
|
||||
}
|
||||
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE);
|
||||
}
|
||||
|
||||
|
@ -241,7 +252,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
|
||||
long threadId = getThreadIdForMessage(id);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
|
@ -304,7 +315,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
ID + " = ?",
|
||||
new String[] {String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))});
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
foundMessage = true;
|
||||
}
|
||||
|
@ -322,6 +333,9 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
}
|
||||
}
|
||||
|
||||
public List<MarkedMessageInfo> setMessagesRead(long threadId, long beforeTime) {
|
||||
return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?", new String[]{threadId+"", beforeTime+""});
|
||||
}
|
||||
public List<MarkedMessageInfo> setMessagesRead(long threadId) {
|
||||
return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", new String[] {String.valueOf(threadId)});
|
||||
}
|
||||
|
@ -385,14 +399,14 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
|
||||
notifyConversationListeners(threadId);
|
||||
notifyConversationListListeners();
|
||||
|
||||
return new Pair<>(messageId, threadId);
|
||||
}
|
||||
|
||||
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
|
||||
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runThreadUpdate) {
|
||||
if (message.isSecureMessage()) {
|
||||
type |= Types.SECURE_MESSAGE_BIT;
|
||||
} else if (message.isGroup()) {
|
||||
|
@ -471,12 +485,8 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long messageId = db.insert(TABLE_NAME, null, values);
|
||||
|
||||
if (unread && runIncrement) {
|
||||
DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1, (message.hasMention() ? 1 : 0));
|
||||
}
|
||||
|
||||
if (runThreadUpdate) {
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
|
||||
}
|
||||
|
||||
if (message.getSubscriptionId() != -1) {
|
||||
|
@ -489,16 +499,16 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
}
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runIncrement, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runIncrement, runThreadUpdate);
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runThreadUpdate);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertCallMessage(IncomingTextMessage message) {
|
||||
return insertMessageInbox(message, 0, 0, true, true);
|
||||
return insertMessageInbox(message, 0, 0, true);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runIncrement, runThreadUpdate);
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runThreadUpdate);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
|
||||
|
@ -552,9 +562,12 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
}
|
||||
|
||||
if (runThreadUpdate) {
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
|
||||
}
|
||||
long lastSeen = DatabaseComponent.get(context).threadDatabase().getLastSeenAndHasSent(threadId).first();
|
||||
if (lastSeen < message.getSentTimestampMillis()) {
|
||||
DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId, message.getSentTimestampMillis());
|
||||
}
|
||||
DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true);
|
||||
|
||||
|
@ -601,7 +614,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
return threadDeleted;
|
||||
}
|
||||
|
@ -625,7 +638,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
|
||||
argValues
|
||||
);
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
return threadDeleted;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue