Compare commits
528 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d955389c46 | |||
| 975eb885c1 | |||
| a3aed96757 | |||
| dc70bfabaf | |||
| 6932340671 | |||
| f6637b7caf | |||
| 4f4be44caa | |||
| 7832497ba7 | |||
| 7d06e2395f | |||
| 3a479d7eef | |||
| 8fe8a1e9ee | |||
| 2d8b2e7fb0 | |||
| 9c0365f92c | |||
| b48abb08d2 | |||
| d8f3e032c7 | |||
| 8dbcb255ad | |||
| db06cbbc86 | |||
| 98ab23c1a3 | |||
| d0ca9ba6a6 | |||
| b242368675 | |||
| 664527ce63 | |||
| 99e4f80be0 | |||
| 702dae9fcd | |||
| 48fe1ba559 | |||
| 382ac7ba0d | |||
| a46f47f352 | |||
| e984d8a42c | |||
| 554bad6b8d | |||
| ed13c97ad7 | |||
| d33873d59a | |||
| 1234899ea1 | |||
| 13027dc44b | |||
| 5b4d74b7fe | |||
| 18c7bc2b5b | |||
| bbbee0f372 | |||
| cf9d090154 | |||
| 718471917f | |||
| bb97407cde | |||
| 92ce678e29 | |||
| e100aea2c7 | |||
| fea3b6cb4a | |||
| afbc132faa | |||
| b27198286d | |||
| ac93d81032 | |||
| 9981e5ca76 | |||
| 7dd3efeb53 | |||
| d38d702adf | |||
| 04a000a8a8 | |||
| 3bbf0741ee | |||
| e9a336100b | |||
| fb600e9829 | |||
| 4a455ff958 | |||
| 707e238e5c | |||
| 90f22a4b66 | |||
| b4f134adf7 | |||
| 1e00fc6149 | |||
| f52133a69c | |||
| 91b142e0d9 | |||
| 26a9dd98c1 | |||
| 99e38e1d23 | |||
| a2d8a25fd9 | |||
| d86d625bcc | |||
| 18e3fb6609 | |||
| da33ba0ed5 | |||
| 66f021d01a | |||
| 40231ea45f | |||
| cd80a47c04 | |||
| 1033bd7bda | |||
| b4f60f3acb | |||
| bed3b571cc | |||
| c8dd4e5254 | |||
| 514048171b | |||
| 32e9901592 | |||
| d83f86a469 | |||
| 403d53586c | |||
| 6acae58694 | |||
| a3f9737e63 | |||
| 263af7c139 | |||
| 7f2439f1e9 | |||
| ae87d23003 | |||
| 3192cc0aac | |||
| 6102e9aa72 | |||
| f4a152b0fe | |||
| 2b11bca7dc | |||
| 07d19f38e3 | |||
| cd228c439e | |||
| 7a859c8961 | |||
| 543f38c75d | |||
| f7b150f2d2 | |||
| 11328f643f | |||
| f270a6b8c4 | |||
| 3fec23fd36 | |||
| e01838e996 | |||
| f70e41e7cd | |||
| c4ec0c9897 | |||
| 989b071a6d | |||
| c39751f9db | |||
| dbf74a2234 | |||
| 837230d72d | |||
| f544ec4126 | |||
| 79dbf85c1e | |||
| 61fe6cc961 | |||
| 70c88b68e2 | |||
| d70c33d20f | |||
| 6b2e000e61 | |||
| b9f11dafff | |||
| 9b32eaeb8a | |||
| a99c0d438e | |||
| c634c24afb | |||
| 2ddd1437cf | |||
| 9da309ca48 | |||
| cfcd451db7 | |||
| 5ab72fd1a9 | |||
| daace9bd1a | |||
| 69adcd1d69 | |||
| 0711a22188 | |||
| 3a06412cd8 | |||
| 51c82702e2 | |||
| 1b01196ec6 | |||
| 1cd6b58ece | |||
| ea8e13b1c8 | |||
| f392229393 | |||
| a299bafe89 | |||
| d2bf539504 | |||
| 903c3989b9 | |||
| 00996f0d7a | |||
| 4aded3a436 | |||
| 9acdc37729 | |||
| d4cdcbe54f | |||
| 6fa2a0f411 | |||
| 558a8e4a14 | |||
| 8947b82034 | |||
| 56551025e9 | |||
| befb4939d5 | |||
| 289f7aba63 | |||
| 28bd245b96 | |||
| c5e7300df2 | |||
| fe25d941bb | |||
| 4cda267f3b | |||
| 82ba7e2b8b | |||
| 41ebaf3938 | |||
| 090c400037 | |||
| 12b1232ac0 | |||
| 204a84c522 | |||
| 526afd539b | |||
| d708984abd | |||
| 9d39db6428 | |||
| 67a8ec0d39 | |||
| 297a7d0ef8 | |||
| 4712833853 | |||
| 11d17f7496 | |||
| 36df3f234f | |||
| 098b298646 | |||
| 2f9320989a | |||
| ec8d5defd4 | |||
| 981676c7f8 | |||
| 7c5ae57784 | |||
| fc7be87468 | |||
| e55d8007fc | |||
| 43b7aa2d52 | |||
| cd1bad0718 | |||
| 6b47618351 | |||
| b6d384120d | |||
| 1268b26c1f | |||
| f1233bfddc | |||
| 1aa3e6afea | |||
| ce21eb241a | |||
| f96fb72eb1 | |||
| 207c467c6b | |||
| 9d1d9e33ed | |||
| e4a76c0690 | |||
| 124c3e25e9 | |||
| 5cb1201903 | |||
| bb6ca80d5a | |||
| dc7c54a1f8 | |||
| 23401440bf | |||
| f8f959e05a | |||
| edbd4d2d03 | |||
| a0b4065be3 | |||
| 1b2f964f32 | |||
| eaf5280d99 | |||
| d435da980f | |||
| 8d3a91f3a4 | |||
| b80c339c5a | |||
| 34159fc9da | |||
| b509ee9ee0 | |||
| a6819448b9 | |||
| f2847f9aa5 | |||
| 8f01e5e1c3 | |||
| acb2f43620 | |||
| 62ac65e4d8 | |||
| 8f183bdcdc | |||
| 3d135d155e | |||
| 090c811391 | |||
| 2a9e8dc525 | |||
| cb0b22cf2c | |||
| 5aba3517ce | |||
| 726f665388 | |||
| e2ac55e9ac | |||
| fa5729bac6 | |||
| e714cb6423 | |||
| 35a0162d5c | |||
| 76740adc3f | |||
| 1c814141a2 | |||
| 5545daf992 | |||
| d300615d90 | |||
| 908a5260c2 | |||
| 7aac6644c3 | |||
| 3b673c07a0 | |||
| d726da822c | |||
| 7894f72b0f | |||
| 4c5822ac67 | |||
| b917cccbee | |||
| 01d2d05d8e | |||
| 4de86cb6cf | |||
| 8861ad76ed | |||
| ef86372635 | |||
| ccff7b1148 | |||
| ed0825112d | |||
| b8df90531f | |||
| f099c3591c | |||
| ed33e048ad | |||
| 7fd3bfa30c | |||
| 07a492a32c | |||
| 11fffbd79e | |||
| eff564ad88 | |||
| d3d53e6099 | |||
| 53d122ed55 | |||
| 1778c1ef7d | |||
| a510bc74e6 | |||
| a9ecdbdfec | |||
| 06ab3cf013 | |||
| 3db5da1c8d | |||
| 5937a50b6d | |||
| b4191ee5cc | |||
| c63e42715e | |||
| 26e582d806 | |||
| ee9270845a | |||
| 6cf33897c0 | |||
| 2161bbb8fa | |||
| b75088874e | |||
| 9ac1897880 | |||
| 36c43ed2fa | |||
| 8084822f16 | |||
| 959718618f | |||
| 75f3fe0cec | |||
| b800477365 | |||
| b191341c57 | |||
| 88a40be901 | |||
| 3fef58057e | |||
| b156e4a79a | |||
| 30ac264cd3 | |||
| a9b00e1cd3 | |||
| d94fc4bc13 | |||
| 40b5339ef8 | |||
| 86f0456e8c | |||
| 48a693793f | |||
| ff28d72db6 | |||
| 456857bbbd | |||
| 7f17b66a6c | |||
| 310ec8f296 | |||
| 0c2afa9438 | |||
| c3832cf8b1 | |||
| a2de8a2a05 | |||
| 3b601896d2 | |||
| e1a90bcb00 | |||
| 2b65916344 | |||
| f149005026 | |||
| 5eb663aa1b | |||
| 12b7d6c0e3 | |||
| 723639d928 | |||
| e0502c24e1 | |||
| 358d6333c3 | |||
| 0b279d1df3 | |||
| 8e0fba7992 | |||
| d5419ec9fa | |||
| 33e3f78be6 | |||
| 3c5ad519dd | |||
| 17c5b858b5 | |||
| f6f6496c9c | |||
| b1d725e23a | |||
| a74622997e | |||
| b1a200001e | |||
| 3b1041fa1f | |||
| a83ccc18bb | |||
| 618b1b5ace | |||
| 14858adc88 | |||
| c07f35f3aa | |||
| 87eab27996 | |||
| b7296a4fe3 | |||
| 3fb9ae1fb4 | |||
| 9705939489 | |||
| eca67b1204 | |||
| c59fc3581a | |||
| e00f8c94ff | |||
| 4186153f0c | |||
| 6c01807f4f | |||
| 9d35fb397b | |||
| c9f2f57427 | |||
| c862ab0c56 | |||
| 7aaaa57c14 | |||
| 11b6394a87 | |||
| bdd48fd2df | |||
| e99af75400 | |||
| 321440e13f | |||
| 0556d984e0 | |||
| 0ba1f66136 | |||
| 7562555687 | |||
| 668ccfcd12 | |||
| 9c0337c4ef | |||
| 3fde06ab0f | |||
| 73959f328a | |||
| cca85bfee3 | |||
| 575caa53d3 | |||
| 33874a8866 | |||
| b8e909a134 | |||
| 5193a5d309 | |||
| 7db288b9aa | |||
| 9f033e64aa | |||
| 5a15ba97dc | |||
| ce6ec72683 | |||
| eedbcdd564 | |||
| 0ca2848e01 | |||
| 208275b6a9 | |||
| 4bdcaa72cd | |||
| 8c6001fa5a | |||
| c4e88abce1 | |||
| eea7174f1d | |||
| 3f7d0688fc | |||
| 6d319618c6 | |||
| 4250fa707b | |||
| 7734cd2c8f | |||
| 57467bb338 | |||
| 8ad61a52b9 | |||
| 9742a212a2 | |||
| fd21fc1a31 | |||
| 1b5a0ab9f3 | |||
| f466fef20a | |||
| 9bc70adbbd | |||
| 6f39f9849a | |||
| 5bc950ed28 | |||
| b80d460a8f | |||
| 3f555ce5e2 | |||
| 9513b476ef | |||
| 8f9e79ae37 | |||
| 53b681ef67 | |||
| 9a8094cb8a | |||
| 00ee6d0bbd | |||
| 83f6640bd3 | |||
| 2afb939ee6 | |||
| 7c442865c5 | |||
| b3d57edb24 | |||
| 6d6e017c71 | |||
| fc6b5c1d7c | |||
| 6ecd3b59fd | |||
| 456bcf3d57 | |||
| f12a9b9ac7 | |||
| 00b6a222bd | |||
| b8ccc4453e | |||
| dbb31420af | |||
| 35f4f3f81e | |||
| acbfff89d3 | |||
| 6b37675a81 | |||
| a471ffa6d8 | |||
| 7bf090fdab | |||
| 4e0279200f | |||
| 78055e3ccb | |||
| f5e6fd6340 | |||
| 2d60d5fb1f | |||
| c6dd25a119 | |||
| 68d29d9a0f | |||
| 1d63970a25 | |||
| 2b1ffac564 | |||
| e2d3a43593 | |||
| 8e13403cca | |||
| 3c6a7b76ca | |||
| 428128651e | |||
| 326678f214 | |||
| 1f994495f8 | |||
| fb1637006d | |||
| 37a35e8f70 | |||
| 1290d0ead9 | |||
| ef0f26b64c | |||
| 485d211768 | |||
| f1ea035197 | |||
| 6f961ade74 | |||
| b8e17e0116 | |||
| 040e1fe8f6 | |||
| e9c92bdf51 | |||
| 48c33f3dcd | |||
| 6b2bc924dd | |||
| a65c4f90f4 | |||
| 04bb4b351a | |||
| e02e4d52b4 | |||
| 6f3c4434f6 | |||
| 711715ca1e | |||
| d6000af843 | |||
| 9b0954a898 | |||
| 42a2c33fd7 | |||
| a4d18a18d9 | |||
| bf32409d4e | |||
| e38aec225f | |||
| 995b7a4712 | |||
| 9fe3026941 | |||
| 520658e1b8 | |||
| f822d8eddb | |||
| 2f879ce4d6 | |||
| 24528bf101 | |||
| 822682caba | |||
| 5dc3cc65a8 | |||
| 0f80caffb5 | |||
| 6c428b2777 | |||
| c9be37b84a | |||
| 87ea2f86c0 | |||
| 7e80be5ca0 | |||
| 989a818a67 | |||
| af2e17df9e | |||
| 728ec1c16d | |||
| f859c5b1b5 | |||
| ab600d7df1 | |||
| 4644f64fd6 | |||
| c274312265 | |||
| f8e63098a2 | |||
| 264d353ec2 | |||
| 2b58dcbe7f | |||
| dc791487c5 | |||
| 5637f132d4 | |||
| 9e6cca1cd0 | |||
| 640c82d517 | |||
| 20d1a93b09 | |||
| f5d1b11bda | |||
| 66c7f8bcb2 | |||
| d8fa46c558 | |||
| 10bfc8a753 | |||
| 9848599807 | |||
| 2e38ebcfbb | |||
| f875623cd0 | |||
| e6f9cb9929 | |||
| 6aac3baa55 | |||
| a860315587 | |||
| a73a73e42c | |||
| a3358e5b21 | |||
| 7e9e2fead2 | |||
| 0269a3eb6f | |||
| f449a45912 | |||
| f69d4ccd22 | |||
| 0e2df2adbb | |||
| d46894e5db | |||
| 951a61117a | |||
| 7a038ab09d | |||
| 707a2aca0a | |||
| 624837fcf1 | |||
| e3ea36c76f | |||
| 453996c374 | |||
| 8add9ba0a6 | |||
| da11b56eab | |||
| 19377c2132 | |||
| b2bff39fe1 | |||
| 5f7075d39a | |||
| 40d9d663ec | |||
| 31f9b77c32 | |||
| 690a66a093 | |||
| e7e7d36774 | |||
| f95a37956c | |||
| 1e2a27f902 | |||
| d90e3dc210 | |||
| 5df4b56c0d | |||
| 436da1cb32 | |||
| 4d0dbbc6cd | |||
| 033bf77cbb | |||
| 1068c3ca7e | |||
| df4422369d | |||
| a62183c9e0 | |||
| de48cf8243 | |||
| acd4fc4518 | |||
| da59ed019f | |||
| e73b174d1d | |||
| 2753a22e3a | |||
| 79fc33630b | |||
| bf5331ba6e | |||
| 3be47d3e54 | |||
| f9de131017 | |||
| f1f505d41c | |||
| 51603be5ec | |||
| 2152b4a2cd | |||
| a70023a32b | |||
| 5038210d78 | |||
| 28bbfd88b2 | |||
| d05a71c8fe | |||
| 245b0a7e50 | |||
| ceb9e4aee2 | |||
| d2e94dad7e | |||
| 240b2108f3 | |||
| f68d99d16d | |||
| 44e845c875 | |||
| d8e2368a18 | |||
| 172a43679d | |||
| 82305ce2b3 | |||
| eaf73edcad | |||
| 543a4ee177 | |||
| fd2a464bae | |||
| b06152ba58 | |||
| c24d285cd3 | |||
| be39cd653e | |||
| 6813f47bc1 | |||
| 8e795c4177 | |||
| 9c96afee09 | |||
| d507be0ab0 | |||
| 75a52f801a | |||
| d3b123f3a9 | |||
| da3cdd984b | |||
| 6184e5f828 | |||
| 133bd44b85 | |||
| 0c254c9621 | |||
| 1faf196f82 | |||
| 81c7887d47 | |||
| e62e630987 | |||
| 739e38a047 | |||
| 8c23b17517 | |||
| fda8f3e1ce | |||
| 9e5f64c431 | |||
| dc689d325b | |||
| 0a883dc234 | |||
| 3824e90997 | |||
| 5158a15379 | |||
| 1bae79af5b | |||
| 58b7612987 | |||
| 9506da6dd3 |
@@ -1,6 +1,11 @@
|
|||||||
name: Android CI
|
name: Android CI
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
- '4.**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -9,11 +14,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: set up JDK 1.8
|
- name: set up JDK 1.8
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 1.8
|
java-version: 1.8
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT}
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew qa
|
run: ./gradlew qa
|
||||||
|
|||||||
@@ -59,9 +59,7 @@ The form and manner of this distribution makes it eligible for export under the
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2011 Whisper Systems
|
Copyright 2013-2020 Signal
|
||||||
|
|
||||||
Copyright 2013-2020 Open Whisper Systems
|
|
||||||
|
|
||||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
lang_map = da_DK:da-rDK,he:iw,id:in,kn_IN:kn-rIN,pt_BR:pt-rBR,pt_PT:pt,qu_EC:qu-rEC,sv_SE:sv-rSE,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW
|
lang_map = da_DK:da-rDK,fil:tl,he:iw,id:in,kn_IN:kn-rIN,pa_PK:pa-rPK,pt_BR:pt-rBR,pt_PT:pt,qu_EC:qu-rEC,sv_SE:sv-rSE,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW
|
||||||
|
|
||||||
[signal-android.master]
|
[signal-android.master]
|
||||||
file_filter = src/main/res/values-<lang>/strings.xml
|
file_filter = src/main/res/values-<lang>/strings.xml
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||||
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
|
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
|
||||||
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
||||||
}
|
}
|
||||||
@@ -80,116 +80,8 @@ protobuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
def canonicalVersionCode = 650
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
|
def canonicalVersionName = "4.62.2"
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
|
||||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
|
||||||
implementation 'androidx.preference:preference:1.0.0'
|
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
|
||||||
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
|
||||||
implementation "androidx.camera:camera-core:1.0.0-alpha06"
|
|
||||||
implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
|
|
||||||
|
|
||||||
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
|
||||||
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.gms:play-services-maps:16.1.0'
|
|
||||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
|
||||||
|
|
||||||
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 project(':libsignal-service')
|
|
||||||
|
|
||||||
implementation 'org.signal:argon2:13.1@aar'
|
|
||||||
|
|
||||||
implementation 'org.signal:ringrtc-android:1.0.1'
|
|
||||||
|
|
||||||
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 'com.github.chrisbanes:PhotoView:2.1.3'
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
|
||||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
|
||||||
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 'mobi.upod:time-duration-picker:1.1.3'
|
|
||||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
|
||||||
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:0.9.1') {
|
|
||||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
|
||||||
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation 'com.airbnb.android:lottie:3.0.7'
|
|
||||||
|
|
||||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
|
||||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
|
||||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
|
||||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
|
||||||
exclude group: 'com.fasterxml.jackson.core'
|
|
||||||
exclude group: 'org.freemarker'
|
|
||||||
}
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
|
||||||
testImplementation 'org.mockito:mockito-core:1.9.5'
|
|
||||||
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:1.2.0'
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.2'
|
|
||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyVerification {
|
|
||||||
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
|
||||||
}
|
|
||||||
|
|
||||||
def canonicalVersionCode = 607
|
|
||||||
def canonicalVersionName = "4.56.1"
|
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['universal' : 0,
|
def abiPostFix = ['universal' : 0,
|
||||||
@@ -223,17 +115,18 @@ android {
|
|||||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
||||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||||
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
buildConfigField "String", "CDS_MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
||||||
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||||
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
|
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
||||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||||
|
|
||||||
@@ -301,12 +194,17 @@ android {
|
|||||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||||
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
|
buildConfigField "String", "CDS_MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
|
||||||
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"a1e9c1d3f352b5c4f0fc7a421b98119e60e5ff703c28fbea85c66bfa7306deab\""
|
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
|
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||||
|
}
|
||||||
|
flipper {
|
||||||
|
initWith debug
|
||||||
|
minifyEnabled false
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
@@ -355,6 +253,132 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
lintChecks project(':lintchecks')
|
||||||
|
|
||||||
|
implementation('androidx.appcompat:appcompat:1.1.0-beta01') {
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
|
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||||
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
implementation 'androidx.preference:preference:1.0.0'
|
||||||
|
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||||
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
|
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||||
|
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||||
|
implementation "androidx.camera:camera-core:1.0.0-beta01"
|
||||||
|
implementation "androidx.camera:camera-camera2:1.0.0-beta01"
|
||||||
|
implementation "androidx.camera:camera-lifecycle:1.0.0-beta01"
|
||||||
|
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||||
|
implementation "androidx.autofill:autofill:1.0.0"
|
||||||
|
implementation "androidx.paging:paging-common:2.1.2"
|
||||||
|
implementation "androidx.paging:paging-runtime:2.1.2"
|
||||||
|
implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
|
||||||
|
implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1'
|
||||||
|
|
||||||
|
implementation ('com.google.firebase:firebase-messaging:20.2.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.gms:play-services-maps:16.1.0'
|
||||||
|
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||||
|
|
||||||
|
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 project(':libsignal-service')
|
||||||
|
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||||
|
|
||||||
|
implementation 'org.signal:argon2:13.1@aar'
|
||||||
|
|
||||||
|
implementation 'org.signal:ringrtc-android:2.0.3'
|
||||||
|
|
||||||
|
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 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||||
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
|
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||||
|
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.melnykov:floatingactionbutton:1.3.0'
|
||||||
|
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||||
|
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||||
|
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
|
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:0.9.1') {
|
||||||
|
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||||
|
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation 'com.airbnb.android:lottie:3.0.7'
|
||||||
|
|
||||||
|
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||||
|
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||||
|
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
||||||
|
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||||
|
exclude group: 'com.fasterxml.jackson.core'
|
||||||
|
exclude group: 'org.freemarker'
|
||||||
|
}
|
||||||
|
implementation 'dnsjava:dnsjava:2.1.9'
|
||||||
|
|
||||||
|
flipperImplementation 'com.facebook.flipper:flipper:0.32.2'
|
||||||
|
flipperImplementation 'com.facebook.soloader:soloader:0.8.2'
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||||
|
testImplementation 'org.mockito:mockito-core:1.9.5'
|
||||||
|
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:1.2.0'
|
||||||
|
testImplementation ('org.robolectric:robolectric:4.2') {
|
||||||
|
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||||
|
}
|
||||||
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
|
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyVerification {
|
||||||
|
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def assembleWebsiteDescriptor = { variant, file ->
|
def assembleWebsiteDescriptor = { variant, file ->
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
|||||||
@@ -17,9 +17,17 @@
|
|||||||
<issue id="ButtonOrder" severity="error" />
|
<issue id="ButtonOrder" severity="error" />
|
||||||
<issue id="ExtraTranslation" severity="warning" />
|
<issue id="ExtraTranslation" severity="warning" />
|
||||||
|
|
||||||
|
<!-- Custom lints -->
|
||||||
|
<issue id="LogNotSignal" severity="error" />
|
||||||
|
<issue id="LogNotAppSignal" severity="error" />
|
||||||
|
<issue id="LogTagInlined" severity="error" />
|
||||||
|
|
||||||
<issue id="RestrictedApi" severity="error">
|
<issue id="RestrictedApi" severity="error">
|
||||||
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java" />
|
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java" />
|
||||||
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java" />
|
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java" />
|
||||||
|
<ignore path="*/org/thoughtcrime/securesms/conversation/*.java" />
|
||||||
|
<ignore path="*/org/thoughtcrime/securesms/lock/v2/CreateKbsPinViewModel.java" />
|
||||||
|
<ignore path="*/org/thoughtcrime/securesms/jobs/StickerPackDownloadJob.java" />
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
</lint>
|
</lint>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "Ottttooooooooo Ocataaaaaaaavius",
|
||||||
|
"number": "+1 (555) 555-5555",
|
||||||
|
"label": "Mobile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Victor Von Doom Phd",
|
||||||
|
"number": "+1 (555) 123-4567",
|
||||||
|
"label": "Home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flash Thompson",
|
||||||
|
"number": "+1 (555) 435-1261",
|
||||||
|
"label": "Work"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dr. Curtis Connors",
|
||||||
|
"number": "+1 (555) 992-1567",
|
||||||
|
"label": "Mobile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Billy Russo",
|
||||||
|
"number": "+1 (555) 234-1516",
|
||||||
|
"label": "Mobile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="org.thoughtcrime.securesms">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".FlipperApplicationContext"
|
||||||
|
tools:replace="android:name"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||||
|
import com.facebook.flipper.core.FlipperClient;
|
||||||
|
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||||
|
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||||
|
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||||
|
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||||
|
import com.facebook.soloader.SoLoader;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter;
|
||||||
|
|
||||||
|
public class FlipperApplicationContext extends ApplicationContext {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
SoLoader.init(this, false);
|
||||||
|
|
||||||
|
FlipperClient client = AndroidFlipperClient.getInstance(this);
|
||||||
|
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
|
||||||
|
client.addPlugin(new DatabasesFlipperPlugin(new FlipperSqlCipherAdapter(this)));
|
||||||
|
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
|
||||||
|
client.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
||||||
|
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
||||||
|
|
||||||
|
import net.sqlcipher.DatabaseUtils;
|
||||||
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
import net.sqlcipher.database.SQLiteStatement;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
|
||||||
|
* and made to work with SqlCipher. Unfortunately I couldn't use it directly, nor subclass it.
|
||||||
|
*/
|
||||||
|
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
||||||
|
|
||||||
|
public FlipperSqlCipherAdapter(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Descriptor> getDatabases() {
|
||||||
|
return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getTableNames(Descriptor descriptor) {
|
||||||
|
SQLiteDatabase db = descriptor.getReadable();
|
||||||
|
List<String> tableNames = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" })) {
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
tableNames.add(cursor.getString(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatabaseGetTableDataResponse getTableData(Descriptor descriptor, String table, String order, boolean reverse, int start, int count) {
|
||||||
|
SQLiteDatabase db = descriptor.getReadable();
|
||||||
|
|
||||||
|
long total = DatabaseUtils.queryNumEntries(db, table);
|
||||||
|
String orderBy = order != null ? order + (reverse ? " DESC" : " ASC") : null;
|
||||||
|
String limitBy = start + ", " + count;
|
||||||
|
|
||||||
|
try (Cursor cursor = db.query(table, null, null, null, null, null, orderBy, limitBy)) {
|
||||||
|
String[] columnNames = cursor.getColumnNames();
|
||||||
|
List<List<Object>> rows = cursorToList(cursor);
|
||||||
|
|
||||||
|
return new DatabaseGetTableDataResponse(Arrays.asList(columnNames), rows, start, rows.size(), total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatabaseGetTableStructureResponse getTableStructure(Descriptor descriptor, String table) {
|
||||||
|
SQLiteDatabase db = descriptor.getReadable();
|
||||||
|
|
||||||
|
Map<String, String> foreignKeyValues = new HashMap<>();
|
||||||
|
|
||||||
|
try(Cursor cursor = db.rawQuery("PRAGMA foreign_key_list(" + table + ")", null)) {
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
String from = cursor.getString(cursor.getColumnIndex("from"));
|
||||||
|
String to = cursor.getString(cursor.getColumnIndex("to"));
|
||||||
|
String tableName = cursor.getString(cursor.getColumnIndex("table")) + "(" + to + ")";
|
||||||
|
|
||||||
|
foreignKeyValues.put(from, tableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
List<String> structureColumns = Arrays.asList("column_name", "data_type", "nullable", "default", "primary_key", "foreign_key");
|
||||||
|
List<List<Object>> structureValues = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) {
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
String columnName = cursor.getString(cursor.getColumnIndex("name"));
|
||||||
|
String foreignKey = foreignKeyValues.containsKey(columnName) ? foreignKeyValues.get(columnName) : null;
|
||||||
|
|
||||||
|
structureValues.add(Arrays.asList(columnName,
|
||||||
|
cursor.getString(cursor.getColumnIndex("type")),
|
||||||
|
cursor.getInt(cursor.getColumnIndex("notnull")) == 0,
|
||||||
|
getObjectFromColumnIndex(cursor, cursor.getColumnIndex("dflt_value")),
|
||||||
|
cursor.getInt(cursor.getColumnIndex("pk")) == 1,
|
||||||
|
foreignKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
List<String> indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name");
|
||||||
|
List<List<Object>> indexesValues = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Cursor indexesCursor = db.rawQuery("PRAGMA index_list(" + table + ")", null)) {
|
||||||
|
List<String> indexedColumnNames = new ArrayList<>();
|
||||||
|
String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name"));
|
||||||
|
|
||||||
|
try(Cursor indexInfoCursor = db.rawQuery("PRAGMA index_info(" + indexName + ")", null)) {
|
||||||
|
while (indexInfoCursor.moveToNext()) {
|
||||||
|
indexedColumnNames.add(indexInfoCursor.getString(indexInfoCursor.getColumnIndex("name")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indexesValues.add(Arrays.asList(indexName,
|
||||||
|
indexesCursor.getInt(indexesCursor.getColumnIndex("unique")) == 1,
|
||||||
|
TextUtils.join(",", indexedColumnNames)));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DatabaseGetTableStructureResponse(structureColumns, structureValues, indexesColumns, indexesValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatabaseGetTableInfoResponse getTableInfo(Descriptor databaseDescriptor, String table) {
|
||||||
|
SQLiteDatabase db = databaseDescriptor.getReadable();
|
||||||
|
|
||||||
|
try (Cursor cursor = db.rawQuery("SELECT sql FROM sqlite_master WHERE name = ?", new String[] { table })) {
|
||||||
|
cursor.moveToFirst();
|
||||||
|
return new DatabaseGetTableInfoResponse(cursor.getString(cursor.getColumnIndex("sql")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatabaseExecuteSqlResponse executeSQL(Descriptor descriptor, String query) {
|
||||||
|
SQLiteDatabase db = descriptor.getWritable();
|
||||||
|
|
||||||
|
String firstWordUpperCase = getFirstWord(query).toUpperCase();
|
||||||
|
|
||||||
|
switch (firstWordUpperCase) {
|
||||||
|
case "UPDATE":
|
||||||
|
case "DELETE":
|
||||||
|
return executeUpdateDelete(db, query);
|
||||||
|
case "INSERT":
|
||||||
|
return executeInsert(db, query);
|
||||||
|
case "SELECT":
|
||||||
|
case "PRAGMA":
|
||||||
|
case "EXPLAIN":
|
||||||
|
return executeSelect(db, query);
|
||||||
|
default:
|
||||||
|
return executeRawQuery(db, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getFirstWord(String s) {
|
||||||
|
s = s.trim();
|
||||||
|
int firstSpace = s.indexOf(' ');
|
||||||
|
return firstSpace >= 0 ? s.substring(0, firstSpace) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DatabaseExecuteSqlResponse executeUpdateDelete(SQLiteDatabase database, String query) {
|
||||||
|
SQLiteStatement statement = database.compileStatement(query);
|
||||||
|
int count = statement.executeUpdateDelete();
|
||||||
|
|
||||||
|
return DatabaseExecuteSqlResponse.successfulUpdateDelete(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DatabaseExecuteSqlResponse executeInsert(SQLiteDatabase database, String query) {
|
||||||
|
SQLiteStatement statement = database.compileStatement(query);
|
||||||
|
long insertedId = statement.executeInsert();
|
||||||
|
|
||||||
|
return DatabaseExecuteSqlResponse.successfulInsert(insertedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DatabaseExecuteSqlResponse executeSelect(SQLiteDatabase database, String query) {
|
||||||
|
try (Cursor cursor = database.rawQuery(query, null)) {
|
||||||
|
String[] columnNames = cursor.getColumnNames();
|
||||||
|
List<List<Object>> rows = cursorToList(cursor);
|
||||||
|
|
||||||
|
return DatabaseExecuteSqlResponse.successfulSelect(Arrays.asList(columnNames), rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DatabaseExecuteSqlResponse executeRawQuery(SQLiteDatabase database, String query) {
|
||||||
|
database.execSQL(query);
|
||||||
|
return DatabaseExecuteSqlResponse.successfulRawQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NonNull List<List<Object>> cursorToList(Cursor cursor) {
|
||||||
|
List<List<Object>> rows = new ArrayList<>();
|
||||||
|
int numColumns = cursor.getColumnCount();
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
List<Object> values = new ArrayList<>(numColumns);
|
||||||
|
|
||||||
|
for (int column = 0; column < numColumns; column++) {
|
||||||
|
values.add(getObjectFromColumnIndex(cursor, column));
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.add(values);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable Object getObjectFromColumnIndex(Cursor cursor, int column) {
|
||||||
|
switch (cursor.getType(column)) {
|
||||||
|
case Cursor.FIELD_TYPE_NULL:
|
||||||
|
return null;
|
||||||
|
case Cursor.FIELD_TYPE_INTEGER:
|
||||||
|
return cursor.getLong(column);
|
||||||
|
case Cursor.FIELD_TYPE_FLOAT:
|
||||||
|
return cursor.getDouble(column);
|
||||||
|
case Cursor.FIELD_TYPE_BLOB:
|
||||||
|
return cursor.getBlob(column);
|
||||||
|
case Cursor.FIELD_TYPE_STRING:
|
||||||
|
default:
|
||||||
|
return cursor.getString(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Descriptor implements DatabaseDescriptor {
|
||||||
|
private final SQLCipherOpenHelper sqlCipherOpenHelper;
|
||||||
|
|
||||||
|
Descriptor(@NonNull SQLCipherOpenHelper sqlCipherOpenHelper) {
|
||||||
|
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return sqlCipherOpenHelper.getDatabaseName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull SQLiteDatabase getReadable() {
|
||||||
|
return sqlCipherOpenHelper.getReadableDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull SQLiteDatabase getWritable() {
|
||||||
|
return sqlCipherOpenHelper.getWritableDatabase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Signal (Flipper)</string>
|
||||||
|
</resources>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.thoughtcrime.securesms">
|
package="org.thoughtcrime.securesms">
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2"/>
|
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" />
|
||||||
|
|
||||||
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
||||||
android:label="Access to TextSecure Secrets"
|
android:label="Access to TextSecure Secrets"
|
||||||
@@ -117,7 +117,9 @@
|
|||||||
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
|
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
|
android:supportsPictureInPicture="true"
|
||||||
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||||
android:launchMode="singleTask"/>
|
android:launchMode="singleTask"/>
|
||||||
|
|
||||||
<activity android:name=".InviteActivity"
|
<activity android:name=".InviteActivity"
|
||||||
@@ -251,6 +253,14 @@
|
|||||||
android:windowSoftInputMode="stateVisible"
|
android:windowSoftInputMode="stateVisible"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||||
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".DatabaseMigrationActivity"
|
<activity android:name=".DatabaseMigrationActivity"
|
||||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
@@ -261,12 +271,7 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".ExperienceUpgradeActivity"
|
<activity android:name=".PassphraseCreateActivity"
|
||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
|
||||||
|
|
||||||
<activity android:name=".PassphraseCreateActivity"
|
|
||||||
android:label="@string/AndroidManifest__create_passphrase"
|
android:label="@string/AndroidManifest__create_passphrase"
|
||||||
android:windowSoftInputMode="stateUnchanged"
|
android:windowSoftInputMode="stateUnchanged"
|
||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
@@ -337,7 +342,7 @@
|
|||||||
android:label="@string/AndroidManifest__linked_devices"
|
android:label="@string/AndroidManifest__linked_devices"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".LogSubmitActivity"
|
<activity android:name=".logsubmit.SubmitDebugLogActivity"
|
||||||
android:label="@string/AndroidManifest__log_submit"
|
android:label="@string/AndroidManifest__log_submit"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
@@ -348,6 +353,11 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity android:name=".AvatarPreviewActivity"
|
||||||
|
android:label="@string/AndroidManifest__media_preview"
|
||||||
|
android:windowSoftInputMode="stateHidden"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
@@ -404,6 +414,10 @@
|
|||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||||
|
android:theme="@style/TextSecure.FullScreenMedia"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".BlockedContactsActivity"
|
<activity android:name=".BlockedContactsActivity"
|
||||||
android:theme="@style/TextSecure.LightTheme"
|
android:theme="@style/TextSecure.LightTheme"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
@@ -412,13 +426,9 @@
|
|||||||
android:theme="@style/TextSecure.DarkTheme"
|
android:theme="@style/TextSecure.DarkTheme"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
|
||||||
android:theme="@style/TextSecure.DarkTheme"/>
|
|
||||||
|
|
||||||
<activity android:name=".profiles.edit.EditProfileActivity"
|
<activity android:name=".profiles.edit.EditProfileActivity"
|
||||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||||
android:windowSoftInputMode="stateVisible"
|
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
|
||||||
|
|
||||||
<activity android:name=".lock.v2.CreateKbsPinActivity"
|
<activity android:name=".lock.v2.CreateKbsPinActivity"
|
||||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||||
@@ -442,6 +452,11 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
|
||||||
|
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".contactshare.ContactShareEditActivity"
|
<activity android:name=".contactshare.ContactShareEditActivity"
|
||||||
android:theme="@style/TextSecure.LightTheme"
|
android:theme="@style/TextSecure.LightTheme"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
@@ -470,10 +485,23 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||||
|
|
||||||
|
<activity android:name=".pin.PinRestoreActivity"
|
||||||
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
|
||||||
|
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.addmembers.AddMembersActivity"
|
||||||
|
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
|
||||||
|
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||||
|
|
||||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||||
<service android:enabled="true" android:name=".service.IncomingMessageObserver$ForegroundService"/>
|
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||||
|
|
||||||
<service android:name=".service.QuickResponseService"
|
<service android:name=".service.QuickResponseService"
|
||||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
||||||
@@ -512,7 +540,9 @@
|
|||||||
|
|
||||||
<service android:name=".service.GenericForegroundService"/>
|
<service android:name=".service.GenericForegroundService"/>
|
||||||
|
|
||||||
<service android:name=".gcm.FcmService">
|
<service android:name=".gcm.FcmFetchService" />
|
||||||
|
|
||||||
|
<service android:name=".gcm.FcmReceiveService">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -624,11 +654,6 @@
|
|||||||
android:authorities="org.thoughtcrime.securesms.database.stickerpack"
|
android:authorities="org.thoughtcrime.securesms.database.stickerpack"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider android:name="androidx.camera.camera2.impl.Camera2Initializer"
|
|
||||||
android:authorities="${applicationId}.camerax-init"
|
|
||||||
android:exported="false"
|
|
||||||
android:enabled="false" />
|
|
||||||
|
|
||||||
<receiver android:name=".service.BootReceiver">
|
<receiver android:name=".service.BootReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
@@ -684,17 +709,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver android:name=".ExperienceUpgradeActivity$AppUpgradeReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
|
||||||
<data android:scheme="package" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.thoughtcrime.securesms.ExperienceUpgradeActivity.DISMISS_ACTION"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".service.PanicResponderListener"
|
android:name=".service.PanicResponderListener"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 415 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 344 KiB |
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 395 KiB |
|
Before Width: | Height: | Size: 494 KiB After Width: | Height: | Size: 622 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 599 KiB |
|
Before Width: | Height: | Size: 952 KiB After Width: | Height: | Size: 559 KiB |
|
Before Width: | Height: | Size: 862 KiB After Width: | Height: | Size: 643 KiB |
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 647 KiB |
|
After Width: | Height: | Size: 602 KiB |
|
After Width: | Height: | Size: 589 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 567 KiB After Width: | Height: | Size: 589 KiB |
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 384 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
|
|
||||||
|
public final class AppCapabilities {
|
||||||
|
|
||||||
|
private AppCapabilities() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final boolean UUID_CAPABLE = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param storageCapable Whether or not the user can use storage service. This is another way of
|
||||||
|
* asking if the user has set a Signal PIN or not.
|
||||||
|
*/
|
||||||
|
public static SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) {
|
||||||
|
return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.insights.InsightsOptOut;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
|
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||||
|
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rule of thumb: if there's something you want to do on the first app launch that involves
|
||||||
|
* persisting state to the database, you'll almost certainly *also* want to do it post backup
|
||||||
|
* restore, since a backup restore will wipe the current state of the database.
|
||||||
|
*/
|
||||||
|
public final class AppInitialization {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(AppInitialization.class);
|
||||||
|
|
||||||
|
private AppInitialization() {}
|
||||||
|
|
||||||
|
public static void onFirstEverAppLaunch(@NonNull Context context) {
|
||||||
|
Log.i(TAG, "onFirstEverAppLaunch()");
|
||||||
|
|
||||||
|
InsightsOptOut.userRequestedOptOut(context);
|
||||||
|
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
|
||||||
|
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||||
|
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||||
|
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||||
|
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||||
|
SignalStore.onFirstEverAppLaunch();
|
||||||
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||||
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||||
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||||
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onPostBackupRestore(@NonNull Context context) {
|
||||||
|
Log.i(TAG, "onPostBackupRestore()");
|
||||||
|
|
||||||
|
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||||
|
SignalStore.onFirstEverAppLaunch();
|
||||||
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||||
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||||
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||||
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,17 +17,15 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
|
||||||
import androidx.camera.camera2.Camera2AppConfig;
|
|
||||||
import androidx.camera.core.CameraX;
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||||
import androidx.multidex.MultiDexApplication;
|
import androidx.multidex.MultiDexApplication;
|
||||||
|
|
||||||
import com.google.android.gms.security.ProviderInstaller;
|
import com.google.android.gms.security.ProviderInstaller;
|
||||||
@@ -42,40 +40,35 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||||
import org.thoughtcrime.securesms.insights.InsightsOptOut;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|
||||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|
||||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
|
||||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||||
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
import org.thoughtcrime.securesms.messages.InitialMessageRetriever;
|
||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||||
|
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||||
|
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
||||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
|
||||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||||
@@ -135,9 +128,12 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
initializePendingMessages();
|
initializePendingMessages();
|
||||||
initializeBlobProvider();
|
initializeBlobProvider();
|
||||||
initializeCleanup();
|
initializeCleanup();
|
||||||
initializeCameraX();
|
|
||||||
FeatureFlags.init();
|
FeatureFlags.init();
|
||||||
NotificationChannels.create(this);
|
NotificationChannels.create(this);
|
||||||
|
RefreshPreKeysJob.scheduleIfNecessary();
|
||||||
|
StorageSyncHelper.scheduleRoutineSync();
|
||||||
|
RegistrationUtil.markRegistrationPossiblyComplete();
|
||||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < 21) {
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
@@ -151,12 +147,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
public void onStart(@NonNull LifecycleOwner owner) {
|
public void onStart(@NonNull LifecycleOwner owner) {
|
||||||
isAppVisible = true;
|
isAppVisible = true;
|
||||||
Log.i(TAG, "App is now visible.");
|
Log.i(TAG, "App is now visible.");
|
||||||
FeatureFlags.refresh();
|
FeatureFlags.refreshIfNecessary();
|
||||||
ApplicationDependencies.getRecipientCache().warmUp();
|
ApplicationDependencies.getRecipientCache().warmUp();
|
||||||
executePendingContactSync();
|
executePendingContactSync();
|
||||||
KeyCachingService.onAppForegrounded(this);
|
KeyCachingService.onAppForegrounded(this);
|
||||||
ApplicationDependencies.getFrameRateTracker().begin();
|
ApplicationDependencies.getFrameRateTracker().begin();
|
||||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||||
|
catchUpOnMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -164,7 +161,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
isAppVisible = false;
|
isAppVisible = false;
|
||||||
Log.i(TAG, "App is no longer visible.");
|
Log.i(TAG, "App is no longer visible.");
|
||||||
KeyCachingService.onAppBackgrounded(this);
|
KeyCachingService.onAppBackgrounded(this);
|
||||||
MessageNotifier.setVisibleThread(-1);
|
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||||
ApplicationDependencies.getFrameRateTracker().end();
|
ApplicationDependencies.getFrameRateTracker().end();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,18 +241,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
|
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
|
||||||
if (!SQLCipherOpenHelper.databaseFileExists(this)) {
|
if (!SQLCipherOpenHelper.databaseFileExists(this)) {
|
||||||
Log.i(TAG, "First ever app launch!");
|
Log.i(TAG, "First ever app launch!");
|
||||||
|
AppInitialization.onFirstEverAppLaunch(this);
|
||||||
InsightsOptOut.userRequestedOptOut(this);
|
|
||||||
TextSecurePreferences.setAppMigrationVersion(this, ApplicationMigrations.CURRENT_VERSION);
|
|
||||||
TextSecurePreferences.setJobManagerVersion(this, JobManager.CURRENT_VERSION);
|
|
||||||
TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode());
|
|
||||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(this, true);
|
|
||||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
|
||||||
SignalStore.registrationValues().onNewInstall();
|
|
||||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
|
||||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
|
||||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
|
||||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
|
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
|
||||||
@@ -392,17 +378,34 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
private void catchUpOnMessages() {
|
||||||
private void initializeCameraX() {
|
InitialMessageRetriever retriever = ApplicationDependencies.getInitialMessageRetriever();
|
||||||
if (CameraXUtil.isSupported()) {
|
|
||||||
new Thread(() -> {
|
if (retriever.isCaughtUp()) {
|
||||||
try {
|
return;
|
||||||
CameraX.init(this, Camera2AppConfig.create(this));
|
|
||||||
} catch (Throwable t) {
|
|
||||||
Log.w(TAG, "Failed to initialize CameraX.");
|
|
||||||
}
|
|
||||||
}, "signal-camerax-initialization").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
switch (retriever.begin(TimeUnit.SECONDS.toMillis(60))) {
|
||||||
|
case SUCCESS:
|
||||||
|
Log.i(TAG, "Successfully caught up on messages. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
break;
|
||||||
|
case FAILURE_TIMEOUT:
|
||||||
|
Log.w(TAG, "Did not finish catching up due to a timeout. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
break;
|
||||||
|
case FAILURE_ERROR:
|
||||||
|
Log.w(TAG, "Did not finish catching up due to an error. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
break;
|
||||||
|
case SKIPPED_ALREADY_CAUGHT_UP:
|
||||||
|
Log.i(TAG, "Already caught up. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
break;
|
||||||
|
case SKIPPED_ALREADY_RUNNING:
|
||||||
|
Log.i(TAG, "Already in the process of catching up. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentManager;
|
|||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.help.HelpFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||||
@@ -66,6 +67,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
|||||||
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
||||||
private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage";
|
private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage";
|
||||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
||||||
|
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
||||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
@@ -154,6 +156,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
|||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_STORAGE));
|
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_STORAGE));
|
||||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
||||||
|
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||||
|
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||||
|
|
||||||
@@ -240,6 +244,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
|||||||
case PREFERENCE_CATEGORY_ADVANCED:
|
case PREFERENCE_CATEGORY_ADVANCED:
|
||||||
fragment = new AdvancedPreferenceFragment();
|
fragment = new AdvancedPreferenceFragment();
|
||||||
break;
|
break;
|
||||||
|
case PREFERENCE_CATEGORY_HELP:
|
||||||
|
fragment = new HelpFragment();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.app.ActivityOptionsCompat;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.bumptech.glide.load.engine.GlideException;
|
||||||
|
import com.bumptech.glide.request.RequestListener;
|
||||||
|
import com.bumptech.glide.request.target.Target;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity for displaying avatars full screen.
|
||||||
|
*/
|
||||||
|
public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActivity {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(AvatarPreviewActivity.class);
|
||||||
|
|
||||||
|
private static final String RECIPIENT_ID_EXTRA = "recipient_id";
|
||||||
|
|
||||||
|
public static @NonNull Intent intentFromRecipientId(@NonNull Context context,
|
||||||
|
@NonNull RecipientId recipientId)
|
||||||
|
{
|
||||||
|
Intent intent = new Intent(context, AvatarPreviewActivity.class);
|
||||||
|
intent.putExtra(RECIPIENT_ID_EXTRA, recipientId.serialize());
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bundle createTransitionBundle(@NonNull Activity activity, @NonNull View from) {
|
||||||
|
return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, from, "avatar").toBundle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||||
|
super.onCreate(savedInstanceState, ready);
|
||||||
|
|
||||||
|
setTheme(R.style.TextSecure_MediaPreview);
|
||||||
|
setContentView(R.layout.contact_photo_preview_activity);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
ImageView avatar = findViewById(R.id.avatar);
|
||||||
|
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
|
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
|
|
||||||
|
showSystemUI();
|
||||||
|
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
|
||||||
|
Context context = getApplicationContext();
|
||||||
|
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
||||||
|
|
||||||
|
Recipient.live(recipientId).observe(this, recipient -> {
|
||||||
|
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||||
|
: recipient.getContactPhoto();
|
||||||
|
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||||
|
: recipient.getFallbackContactPhoto();
|
||||||
|
|
||||||
|
GlideApp.with(this).load(contactPhoto)
|
||||||
|
.fallback(fallbackPhoto.asCallCard(this))
|
||||||
|
.error(fallbackPhoto.asCallCard(this))
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
|
.addListener(new RequestListener<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||||
|
Log.w(TAG, "Unable to load avatar, or avatar removed, closing");
|
||||||
|
finish();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(avatar);
|
||||||
|
|
||||||
|
toolbar.setTitle(recipient.toShortString(context));
|
||||||
|
});
|
||||||
|
|
||||||
|
avatar.setOnClickListener(v -> toggleUiVisibility());
|
||||||
|
|
||||||
|
showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
|
||||||
|
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
|
||||||
|
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
|
||||||
|
|
||||||
|
for (View view : views) {
|
||||||
|
view.animate()
|
||||||
|
.alpha(hide ? 0 : 1)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleUiVisibility() {
|
||||||
|
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
|
||||||
|
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
|
||||||
|
showSystemUI();
|
||||||
|
} else {
|
||||||
|
hideSystemUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||||
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSystemUI() {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSupportNavigateUp() {
|
||||||
|
onBackPressed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
public class BasicIntroFragment extends Fragment {
|
|
||||||
|
|
||||||
private static final String ARG_DRAWABLE = "drawable";
|
|
||||||
private static final String ARG_TEXT = "text";
|
|
||||||
private static final String ARG_SUBTEXT = "subtext";
|
|
||||||
|
|
||||||
private int drawable;
|
|
||||||
private int text;
|
|
||||||
private int subtext;
|
|
||||||
|
|
||||||
public static BasicIntroFragment newInstance(int drawable, int text, int subtext) {
|
|
||||||
BasicIntroFragment fragment = new BasicIntroFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putInt(ARG_DRAWABLE, drawable);
|
|
||||||
args.putInt(ARG_TEXT, text);
|
|
||||||
args.putInt(ARG_SUBTEXT, subtext);
|
|
||||||
fragment.setArguments(args);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BasicIntroFragment() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
if (getArguments() != null) {
|
|
||||||
drawable = getArguments().getInt(ARG_DRAWABLE);
|
|
||||||
text = getArguments().getInt(ARG_TEXT);
|
|
||||||
subtext = getArguments().getInt(ARG_SUBTEXT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
View v = inflater.inflate(R.layout.color_fragment, container, false);
|
|
||||||
|
|
||||||
((ImageView)v.findViewById(R.id.watermark)).setImageResource(drawable);
|
|
||||||
((TextView)v.findViewById(R.id.blurb)).setText(text);
|
|
||||||
((TextView)v.findViewById(R.id.subblurb)).setText(subtext);
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.contactshare.Contact;
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
@@ -45,5 +46,6 @@ public interface BindableConversationItem extends Unbindable {
|
|||||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||||
void onReactionClicked(long messageId, boolean isMms);
|
void onReactionClicked(long messageId, boolean isMms);
|
||||||
|
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import java.util.Set;
|
|||||||
|
|
||||||
public interface BindableConversationListItem extends Unbindable {
|
public interface BindableConversationListItem extends Unbindable {
|
||||||
|
|
||||||
public void bind(@NonNull ThreadRecord thread,
|
void bind(@NonNull ThreadRecord thread,
|
||||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||||
@NonNull Set<Long> typingThreads,
|
@NonNull Set<Long> typingThreads,
|
||||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should be used whenever we want to prompt the user to block/unblock a recipient.
|
||||||
|
*/
|
||||||
|
public final class BlockUnblockDialog {
|
||||||
|
|
||||||
|
private BlockUnblockDialog() { }
|
||||||
|
|
||||||
|
public static void showBlockFor(@NonNull Context context,
|
||||||
|
@NonNull Lifecycle lifecycle,
|
||||||
|
@NonNull Recipient recipient,
|
||||||
|
@NonNull Runnable onBlock)
|
||||||
|
{
|
||||||
|
SimpleTask.run(lifecycle,
|
||||||
|
() -> buildBlockFor(context, recipient, onBlock, null),
|
||||||
|
AlertDialog.Builder::show);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showBlockAndDeleteFor(@NonNull Context context,
|
||||||
|
@NonNull Lifecycle lifecycle,
|
||||||
|
@NonNull Recipient recipient,
|
||||||
|
@NonNull Runnable onBlock,
|
||||||
|
@NonNull Runnable onBlockAndDelete)
|
||||||
|
{
|
||||||
|
SimpleTask.run(lifecycle,
|
||||||
|
() -> buildBlockFor(context, recipient, onBlock, onBlockAndDelete),
|
||||||
|
AlertDialog.Builder::show);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showUnblockFor(@NonNull Context context,
|
||||||
|
@NonNull Lifecycle lifecycle,
|
||||||
|
@NonNull Recipient recipient,
|
||||||
|
@NonNull Runnable onUnblock)
|
||||||
|
{
|
||||||
|
SimpleTask.run(lifecycle,
|
||||||
|
() -> buildUnblockFor(context, recipient, onUnblock),
|
||||||
|
AlertDialog.Builder::show);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private static AlertDialog.Builder buildBlockFor(@NonNull Context context,
|
||||||
|
@NonNull Recipient recipient,
|
||||||
|
@NonNull Runnable onBlock,
|
||||||
|
@Nullable Runnable onBlockAndDelete)
|
||||||
|
{
|
||||||
|
recipient = recipient.resolve();
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
|
Resources resources = context.getResources();
|
||||||
|
|
||||||
|
if (recipient.isGroup()) {
|
||||||
|
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
|
||||||
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
|
||||||
|
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
|
||||||
|
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
} else {
|
||||||
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||||
|
builder.setMessage(R.string.BlockUnblockDialog_group_members_wont_be_able_to_add_you);
|
||||||
|
builder.setPositiveButton(R.string.RecipientPreferenceActivity_block, ((dialog, which) -> onBlock.run()));
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||||
|
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
|
||||||
|
|
||||||
|
if (onBlockAndDelete != null) {
|
||||||
|
builder.setNeutralButton(android.R.string.cancel, null);
|
||||||
|
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_delete, (d, w) -> onBlockAndDelete.run());
|
||||||
|
builder.setNegativeButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run());
|
||||||
|
} else {
|
||||||
|
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private static AlertDialog.Builder buildUnblockFor(@NonNull Context context,
|
||||||
|
@NonNull Recipient recipient,
|
||||||
|
@NonNull Runnable onUnblock)
|
||||||
|
{
|
||||||
|
recipient = recipient.resolve();
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
|
Resources resources = context.getResources();
|
||||||
|
|
||||||
|
if (recipient.isGroup()) {
|
||||||
|
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
|
||||||
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||||
|
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
|
||||||
|
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
} else {
|
||||||
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||||
|
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
|
||||||
|
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||||
|
builder.setMessage(R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other);
|
||||||
|
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.ListFragment;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.cursoradapter.widget.CursorAdapter;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -17,6 +10,13 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.cursoradapter.widget.CursorAdapter;
|
||||||
|
import androidx.fragment.app.ListFragment;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
import androidx.loader.content.Loader;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
import org.thoughtcrime.securesms.database.loaders.BlockedContactsLoader;
|
import org.thoughtcrime.securesms.database.loaders.BlockedContactsLoader;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
@@ -25,21 +25,18 @@ import org.thoughtcrime.securesms.preferences.BlockedContactListItem;
|
|||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
|
||||||
public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity {
|
public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity {
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreCreate() {
|
public void onPreCreate() {
|
||||||
dynamicTheme.onCreate(this);
|
dynamicTheme.onCreate(this);
|
||||||
dynamicLanguage.onCreate(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle bundle, boolean ready) {
|
public void onCreate(Bundle bundle, boolean ready) {
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
@@ -51,16 +48,12 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
dynamicTheme.onResume(this);
|
dynamicTheme.onResume(this);
|
||||||
dynamicLanguage.onResume(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onSupportNavigateUp() {
|
||||||
switch (item.getItemId()) {
|
onBackPressed();
|
||||||
case android.R.id.home: finish(); return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BlockedContactsFragment
|
public static class BlockedContactsFragment
|
||||||
@@ -76,14 +69,14 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle bundle) {
|
public void onCreate(Bundle bundle) {
|
||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
setListAdapter(new BlockedContactAdapter(getActivity(), GlideApp.with(this), null));
|
setListAdapter(new BlockedContactAdapter(requireActivity(), GlideApp.with(this), null));
|
||||||
getLoaderManager().initLoader(0, null, this);
|
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
getLoaderManager().restartLoader(0, null, this);
|
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -114,10 +107,10 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
Recipient recipient = ((BlockedContactListItem)view).getRecipient();
|
Recipient recipient = ((BlockedContactListItem)view).getRecipient();
|
||||||
Intent intent = new Intent(getActivity(), RecipientPreferenceActivity.class);
|
BlockUnblockDialog.showUnblockFor(requireContext(), getLifecycle(), recipient, () -> {
|
||||||
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId());
|
RecipientUtil.unblock(requireContext(), recipient);
|
||||||
|
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||||
startActivity(intent);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class BlockedContactAdapter extends CursorAdapter {
|
private static class BlockedContactAdapter extends CursorAdapter {
|
||||||
@@ -143,7 +136,5 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
((BlockedContactListItem) view).set(glideRequests, recipient);
|
((BlockedContactListItem) view).set(glideRequests, recipient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,47 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
|
|
||||||
public class ClearProfileAvatarActivity extends Activity {
|
public class ClearProfileAvatarActivity extends Activity {
|
||||||
|
|
||||||
|
private static final String ARG_TITLE = "arg_title";
|
||||||
|
|
||||||
|
public static Intent createForUserProfilePhoto() {
|
||||||
|
return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Intent createForGroupProfilePhoto() {
|
||||||
|
Intent intent = new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
|
||||||
|
intent.putExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_group_photo);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
new AlertDialog.Builder(this)
|
int titleId = getIntent().getIntExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo);
|
||||||
.setTitle(R.string.ClearProfileActivity_remove_profile_photo)
|
|
||||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
|
new AlertDialog.Builder(new ContextThemeWrapper(this, DynamicTheme.isDarkTheme(this) ? R.style.TextSecure_DarkTheme : R.style.TextSecure_LightTheme))
|
||||||
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
|
.setMessage(titleId)
|
||||||
Intent result = new Intent();
|
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
|
||||||
result.putExtra("delete", true);
|
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
|
||||||
setResult(Activity.RESULT_OK, result);
|
Intent result = new Intent();
|
||||||
finish();
|
result.putExtra("delete", true);
|
||||||
})
|
setResult(Activity.RESULT_OK, result);
|
||||||
.show();
|
finish();
|
||||||
|
})
|
||||||
|
.setOnCancelListener(dialog -> finish())
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,18 @@ package org.thoughtcrime.securesms;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -46,12 +44,14 @@ import java.lang.ref.WeakReference;
|
|||||||
*/
|
*/
|
||||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
|
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
|
||||||
implements SwipeRefreshLayout.OnRefreshListener,
|
implements SwipeRefreshLayout.OnRefreshListener,
|
||||||
ContactSelectionListFragment.OnContactSelectedListener
|
ContactSelectionListFragment.OnContactSelectedListener,
|
||||||
|
ContactSelectionListFragment.ScrollCallback
|
||||||
{
|
{
|
||||||
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
|
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
public static final String EXTRA_LAYOUT_RES_ID = "layout_res_id";
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
|
||||||
|
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||||
|
|
||||||
protected ContactSelectionListFragment contactsFragment;
|
protected ContactSelectionListFragment contactsFragment;
|
||||||
|
|
||||||
@@ -60,18 +60,17 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
|||||||
@Override
|
@Override
|
||||||
protected void onPreCreate() {
|
protected void onPreCreate() {
|
||||||
dynamicTheme.onCreate(this);
|
dynamicTheme.onCreate(this);
|
||||||
dynamicLanguage.onCreate(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle icicle, boolean ready) {
|
protected void onCreate(Bundle icicle, boolean ready) {
|
||||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||||
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
|
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
|
||||||
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS;
|
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
setContentView(R.layout.contact_selection_activity);
|
setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity));
|
||||||
|
|
||||||
initializeToolbar();
|
initializeToolbar();
|
||||||
initializeResources();
|
initializeResources();
|
||||||
@@ -82,7 +81,6 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
dynamicTheme.onResume(this);
|
dynamicTheme.onResume(this);
|
||||||
dynamicLanguage.onResume(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ContactFilterToolbar getToolbar() {
|
protected ContactFilterToolbar getToolbar() {
|
||||||
@@ -90,10 +88,9 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeToolbar() {
|
private void initializeToolbar() {
|
||||||
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
|
this.toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
assert getSupportActionBar() != null;
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||||
getSupportActionBar().setIcon(null);
|
getSupportActionBar().setIcon(null);
|
||||||
@@ -121,6 +118,17 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
|||||||
@Override
|
@Override
|
||||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBeginScroll() {
|
||||||
|
hideKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideKeyboard() {
|
||||||
|
ServiceUtil.getInputMethodManager(this)
|
||||||
|
.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
|
||||||
|
toolbar.clearFocus();
|
||||||
|
}
|
||||||
|
|
||||||
private static class RefreshDirectoryTask extends AsyncTask<Context, Void, Void> {
|
private static class RefreshDirectoryTask extends AsyncTask<Context, Void, Void> {
|
||||||
|
|
||||||
private final WeakReference<ContactSelectionActivity> activity;
|
private final WeakReference<ContactSelectionActivity> activity;
|
||||||
|
|||||||
@@ -18,47 +18,63 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.animation.LayoutTransition;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.view.animation.CycleInterpolator;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.HorizontalScrollView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
|
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
import androidx.transition.AutoTransition;
|
||||||
|
import androidx.transition.TransitionManager;
|
||||||
|
|
||||||
|
import com.annimon.stream.Collectors;
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
import com.google.android.material.chip.ChipGroup;
|
||||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||||
|
import org.thoughtcrime.securesms.contacts.ContactChip;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||||
|
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
@@ -66,8 +82,9 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
|||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,13 +99,20 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||||
|
|
||||||
public static final String DISPLAY_MODE = "display_mode";
|
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
|
||||||
public static final String MULTI_SELECT = "multi_select";
|
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
|
||||||
public static final String REFRESHABLE = "refreshable";
|
|
||||||
public static final String RECENTS = "recents";
|
|
||||||
|
|
||||||
|
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
public static final String DISPLAY_MODE = "display_mode";
|
||||||
|
public static final String MULTI_SELECT = "multi_select";
|
||||||
|
public static final String REFRESHABLE = "refreshable";
|
||||||
|
public static final String RECENTS = "recents";
|
||||||
|
public static final String TOTAL_CAPACITY = "total_capacity";
|
||||||
|
public static final String CURRENT_SELECTION = "current_selection";
|
||||||
|
|
||||||
|
private ConstraintLayout constraintLayout;
|
||||||
private TextView emptyText;
|
private TextView emptyText;
|
||||||
private Set<SelectedContact> selectedContacts;
|
|
||||||
private OnContactSelectedListener onContactSelectedListener;
|
private OnContactSelectedListener onContactSelectedListener;
|
||||||
private SwipeRefreshLayout swipeRefresh;
|
private SwipeRefreshLayout swipeRefresh;
|
||||||
private View showContactsLayout;
|
private View showContactsLayout;
|
||||||
@@ -99,16 +123,28 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private RecyclerViewFastScroller fastScroller;
|
private RecyclerViewFastScroller fastScroller;
|
||||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||||
|
private ChipGroup chipGroup;
|
||||||
|
private HorizontalScrollView chipGroupScrollContainer;
|
||||||
|
private TextView groupLimit;
|
||||||
|
|
||||||
|
@Nullable private FixedViewsAdapter headerAdapter;
|
||||||
@Nullable private FixedViewsAdapter footerAdapter;
|
@Nullable private FixedViewsAdapter footerAdapter;
|
||||||
@Nullable private InviteCallback inviteCallback;
|
@Nullable private ListCallback listCallback;
|
||||||
|
@Nullable private ScrollCallback scrollCallback;
|
||||||
|
private GlideRequests glideRequests;
|
||||||
|
private int selectionLimit;
|
||||||
|
private Set<RecipientId> currentSelection;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
|
|
||||||
if (context instanceof InviteCallback) {
|
if (context instanceof ListCallback) {
|
||||||
inviteCallback = (InviteCallback) context;
|
listCallback = (ListCallback) context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context instanceof ScrollCallback) {
|
||||||
|
scrollCallback = (ScrollCallback) context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,14 +166,16 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
||||||
handleContactPermissionGranted();
|
handleContactPermissionGranted();
|
||||||
} else {
|
} else {
|
||||||
this.getLoaderManager().initLoader(0, null, this);
|
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onAnyDenied(() -> {
|
.onAnyDenied(() -> {
|
||||||
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
FragmentActivity activity = requireActivity();
|
||||||
|
|
||||||
if (getActivity().getIntent().getBooleanExtra(RECENTS, false)) {
|
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||||
getLoaderManager().initLoader(0, null, ContactSelectionListFragment.this);
|
|
||||||
|
if (activity.getIntent().getBooleanExtra(RECENTS, false)) {
|
||||||
|
LoaderManager.getInstance(this).initLoader(0, null, ContactSelectionListFragment.this);
|
||||||
} else {
|
} else {
|
||||||
initializeNoContactsPermission();
|
initializeNoContactsPermission();
|
||||||
}
|
}
|
||||||
@@ -149,64 +187,140 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
|
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
|
||||||
|
|
||||||
emptyText = ViewUtil.findById(view, android.R.id.empty);
|
emptyText = view.findViewById(android.R.id.empty);
|
||||||
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
recyclerView = view.findViewById(R.id.recycler_view);
|
||||||
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
|
swipeRefresh = view.findViewById(R.id.swipe_refresh);
|
||||||
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
|
fastScroller = view.findViewById(R.id.fast_scroller);
|
||||||
showContactsLayout = view.findViewById(R.id.show_contacts_container);
|
showContactsLayout = view.findViewById(R.id.show_contacts_container);
|
||||||
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
||||||
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
||||||
showContactsProgress = view.findViewById(R.id.progress);
|
showContactsProgress = view.findViewById(R.id.progress);
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
chipGroup = view.findViewById(R.id.chipGroup);
|
||||||
|
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
|
||||||
|
groupLimit = view.findViewById(R.id.group_limit);
|
||||||
|
constraintLayout = view.findViewById(R.id.container);
|
||||||
|
|
||||||
swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||||
|
recyclerView.setItemAnimator(new DefaultItemAnimator() {
|
||||||
|
@Override
|
||||||
|
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||||
|
|
||||||
|
selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
|
||||||
|
currentSelection = getCurrentSelection();
|
||||||
|
|
||||||
|
updateGroupLimit(getChipCount());
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateGroupLimit(int chipCount) {
|
||||||
|
if (selectionLimit != NO_LIMIT) {
|
||||||
|
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
|
||||||
|
groupLimit.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
groupLimit.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull List<SelectedContact> getSelectedContacts() {
|
public @NonNull List<SelectedContact> getSelectedContacts() {
|
||||||
List<SelectedContact> selected = new LinkedList<>();
|
if (cursorRecyclerViewAdapter == null) {
|
||||||
if (selectedContacts != null) {
|
return Collections.emptyList();
|
||||||
selected.addAll(selectedContacts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return selected;
|
return cursorRecyclerViewAdapter.getSelectedContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectedContactsCount() {
|
||||||
|
if (cursorRecyclerViewAdapter == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<RecipientId> getCurrentSelection() {
|
||||||
|
List<RecipientId> currentSelection = requireActivity().getIntent().getParcelableArrayListExtra(CURRENT_SELECTION);
|
||||||
|
|
||||||
|
return currentSelection == null ? Collections.emptySet()
|
||||||
|
: Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isMulti() {
|
private boolean isMulti() {
|
||||||
return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeCursor() {
|
private void initializeCursor() {
|
||||||
|
glideRequests = GlideApp.with(this);
|
||||||
|
|
||||||
cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(),
|
cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(),
|
||||||
GlideApp.with(this),
|
glideRequests,
|
||||||
null,
|
null,
|
||||||
new ListClickListener(),
|
new ListClickListener(),
|
||||||
isMulti());
|
isMulti(),
|
||||||
selectedContacts = cursorRecyclerViewAdapter.getSelectedContacts();
|
currentSelection);
|
||||||
|
|
||||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||||
|
|
||||||
|
if (listCallback != null && FeatureFlags.newGroupUI()) {
|
||||||
|
if (FeatureFlags.groupsV2create() && FeatureFlags.groupsV2internalTest()) {
|
||||||
|
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback), createNewGroupsV1GroupItem(listCallback));
|
||||||
|
} else {
|
||||||
|
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
|
||||||
|
}
|
||||||
|
headerAdapter.hide();
|
||||||
|
concatenateAdapter.addAdapter(headerAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
concatenateAdapter.addAdapter(cursorRecyclerViewAdapter);
|
concatenateAdapter.addAdapter(cursorRecyclerViewAdapter);
|
||||||
if (inviteCallback != null) {
|
|
||||||
footerAdapter = new FixedViewsAdapter(createInviteActionView(inviteCallback));
|
if (listCallback != null) {
|
||||||
|
footerAdapter = new FixedViewsAdapter(createInviteActionView(listCallback));
|
||||||
footerAdapter.hide();
|
footerAdapter.hide();
|
||||||
concatenateAdapter.addAdapter(footerAdapter);
|
concatenateAdapter.addAdapter(footerAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
recyclerView.setAdapter(concatenateAdapter);
|
recyclerView.setAdapter(concatenateAdapter);
|
||||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
|
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
|
||||||
|
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||||
|
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
|
||||||
|
if (scrollCallback != null) {
|
||||||
|
scrollCallback.onBeginScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private View createInviteActionView(@NonNull InviteCallback inviteCallback) {
|
private View createInviteActionView(@NonNull ListCallback listCallback) {
|
||||||
View view = LayoutInflater.from(requireContext())
|
View view = LayoutInflater.from(requireContext())
|
||||||
.inflate(R.layout.contact_selection_invite_action_item, (ViewGroup) requireView(), false);
|
.inflate(R.layout.contact_selection_invite_action_item, (ViewGroup) requireView(), false);
|
||||||
view.setOnClickListener(v -> inviteCallback.onInvite());
|
view.setOnClickListener(v -> listCallback.onInvite());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createNewGroupItem(@NonNull ListCallback listCallback) {
|
||||||
|
View view = LayoutInflater.from(requireContext())
|
||||||
|
.inflate(R.layout.contact_selection_new_group_item, (ViewGroup) requireView(), false);
|
||||||
|
view.setOnClickListener(v -> listCallback.onNewGroup(false));
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createNewGroupsV1GroupItem(@NonNull ListCallback listCallback) {
|
||||||
|
View view = LayoutInflater.from(requireContext())
|
||||||
|
.inflate(R.layout.contact_selection_new_group_v1_item, (ViewGroup) requireView(), false);
|
||||||
|
view.setOnClickListener(v -> listCallback.onNewGroup(true));
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,23 +356,28 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
swipeRefresh.setRefreshing(false);
|
swipeRefresh.setRefreshing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasQueryFilter() {
|
||||||
|
return !TextUtils.isEmpty(cursorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
public void setRefreshing(boolean refreshing) {
|
public void setRefreshing(boolean refreshing) {
|
||||||
swipeRefresh.setRefreshing(refreshing);
|
swipeRefresh.setRefreshing(refreshing);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
selectedContacts.clear();
|
cursorRecyclerViewAdapter.clearSelectedContacts();
|
||||||
|
|
||||||
if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) {
|
if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) {
|
||||||
getLoaderManager().restartLoader(0, null, this);
|
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
return new ContactsCursorLoader(getActivity(),
|
FragmentActivity activity = requireActivity();
|
||||||
getActivity().getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL),
|
return new ContactsCursorLoader(activity,
|
||||||
cursorFilter, getActivity().getIntent().getBooleanExtra(RECENTS, false));
|
activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL),
|
||||||
|
cursorFilter, activity.getIntent().getBooleanExtra(RECENTS, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -272,6 +391,14 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
footerAdapter.show();
|
footerAdapter.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (headerAdapter != null) {
|
||||||
|
if (TextUtils.isEmpty(cursorFilter)) {
|
||||||
|
headerAdapter.show();
|
||||||
|
} else {
|
||||||
|
headerAdapter.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
||||||
boolean useFastScroller = data != null && data.getCount() > 20;
|
boolean useFastScroller = data != null && data.getCount() > 20;
|
||||||
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
||||||
@@ -336,7 +463,13 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||||
|
|
||||||
if (!isMulti() || !selectedContacts.contains(selectedContact)) {
|
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||||
|
if (selectionLimitReached()) {
|
||||||
|
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
|
||||||
|
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (contact.isUsernameType()) {
|
if (contact.isUsernameType()) {
|
||||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||||
|
|
||||||
@@ -346,8 +479,9 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
loadingDialog.dismiss();
|
loadingDialog.dismiss();
|
||||||
if (uuid.isPresent()) {
|
if (uuid.isPresent()) {
|
||||||
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
||||||
selectedContacts.add(SelectedContact.forUsername(recipient.getId(), contact.getNumber()));
|
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||||
contact.setChecked(true);
|
markContactSelected(selected);
|
||||||
|
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
|
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
|
||||||
@@ -361,22 +495,126 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
selectedContacts.add(selectedContact);
|
markContactSelected(selectedContact);
|
||||||
contact.setChecked(true);
|
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
|
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedContacts.remove(selectedContact);
|
markContactUnselected(selectedContact);
|
||||||
contact.setChecked(false);
|
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean selectionLimitReached() {
|
||||||
|
return getChipCount() >= selectionLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||||
|
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
||||||
|
if (isMulti() && FeatureFlags.newGroupUI()) {
|
||||||
|
addChipForSelectedContact(selectedContact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
||||||
|
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
||||||
|
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||||
|
removeChipForContact(selectedContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeChipForContact(@NonNull SelectedContact contact) {
|
||||||
|
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) {
|
||||||
|
View v = chipGroup.getChildAt(i);
|
||||||
|
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
|
||||||
|
chipGroup.removeView(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateGroupLimit(getChipCount());
|
||||||
|
|
||||||
|
if (getChipCount() == 0) {
|
||||||
|
setChipGroupVisibility(ConstraintSet.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
|
||||||
|
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
|
||||||
|
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||||
|
resolved -> addChipForRecipient(resolved, selectedContact));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
|
||||||
|
final ContactChip chip = new ContactChip(requireContext());
|
||||||
|
|
||||||
|
if (getChipCount() == 0) {
|
||||||
|
setChipGroupVisibility(ConstraintSet.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
chip.setText(recipient.getShortDisplayName(requireContext()));
|
||||||
|
chip.setContact(selectedContact);
|
||||||
|
chip.setCloseIconVisible(true);
|
||||||
|
chip.setOnCloseIconClickListener(view -> {
|
||||||
|
markContactUnselected(selectedContact);
|
||||||
|
|
||||||
|
if (onContactSelectedListener != null) {
|
||||||
|
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
|
||||||
|
@Override
|
||||||
|
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||||
|
if (view == chip && transitionType == LayoutTransition.APPEARING) {
|
||||||
|
chipGroup.getLayoutTransition().removeTransitionListener(this);
|
||||||
|
registerChipRecipientObserver(chip, recipient.live());
|
||||||
|
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addChip(@NonNull ContactChip chip) {
|
||||||
|
chipGroup.addView(chip);
|
||||||
|
updateGroupLimit(getChipCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getChipCount() {
|
||||||
|
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
||||||
|
if (count < 0) throw new AssertionError();
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
|
||||||
|
if (recipient != null) {
|
||||||
|
recipient.observe(getViewLifecycleOwner(), resolved -> {
|
||||||
|
if (chip.isAttachedToWindow()) {
|
||||||
|
chip.setAvatar(glideRequests, resolved, null);
|
||||||
|
chip.setText(resolved.getShortDisplayName(chip.getContext()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setChipGroupVisibility(int visibility) {
|
||||||
|
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
|
||||||
|
|
||||||
|
ConstraintSet constraintSet = new ConstraintSet();
|
||||||
|
constraintSet.clone(constraintLayout);
|
||||||
|
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility);
|
||||||
|
constraintSet.applyTo(constraintLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
|
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
|
||||||
@@ -387,12 +625,22 @@ public final class ContactSelectionListFragment extends Fragment
|
|||||||
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
|
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void smoothScrollChipsToEnd() {
|
||||||
|
int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0;
|
||||||
|
chipGroupScrollContainer.smoothScrollTo(x, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public interface OnContactSelectedListener {
|
public interface OnContactSelectedListener {
|
||||||
void onContactSelected(Optional<RecipientId> recipientId, String number);
|
void onContactSelected(Optional<RecipientId> recipientId, String number);
|
||||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface InviteCallback {
|
public interface ListCallback {
|
||||||
void onInvite();
|
void onInvite();
|
||||||
|
void onNewGroup(boolean forceV1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ScrollCallback {
|
||||||
|
void onBeginScroll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.ListFragment;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.SimpleAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.loaders.CountryListLoader;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class CountrySelectionFragment extends ListFragment implements LoaderManager.LoaderCallbacks<ArrayList<Map<String, String>>> {
|
|
||||||
|
|
||||||
private EditText countryFilter;
|
|
||||||
private CountrySelectedListener listener;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
|
||||||
return inflater.inflate(R.layout.country_selection_fragment, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityCreated(Bundle bundle) {
|
|
||||||
super.onActivityCreated(bundle);
|
|
||||||
this.countryFilter = (EditText)getView().findViewById(R.id.country_search);
|
|
||||||
this.countryFilter.addTextChangedListener(new FilterWatcher());
|
|
||||||
getLoaderManager().initLoader(0, null, this).forceLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Activity activity) {
|
|
||||||
super.onAttach(activity);
|
|
||||||
this.listener = (CountrySelectedListener)activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onListItemClick(ListView listView, View view, int position, long id) {
|
|
||||||
Map<String, String> item = (Map<String, String>)this.getListAdapter().getItem(position);
|
|
||||||
if (this.listener != null) {
|
|
||||||
this.listener.countrySelected(item.get("country_name"),
|
|
||||||
Integer.parseInt(item.get("country_code").substring(1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull Loader<ArrayList<Map<String, String>>> onCreateLoader(int arg0, Bundle arg1) {
|
|
||||||
return new CountryListLoader(getActivity());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(@NonNull Loader<ArrayList<Map<String, String>>> loader,
|
|
||||||
ArrayList<Map<String, String>> results)
|
|
||||||
{
|
|
||||||
String[] from = {"country_name", "country_code"};
|
|
||||||
int[] to = {R.id.country_name, R.id.country_code};
|
|
||||||
this.setListAdapter(new SimpleAdapter(getActivity(), results, R.layout.country_list_item, from, to));
|
|
||||||
|
|
||||||
if (this.countryFilter != null && this.countryFilter.getText().length() != 0) {
|
|
||||||
((SimpleAdapter)getListAdapter()).getFilter().filter(this.countryFilter.getText().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<ArrayList<Map<String, String>>> arg0) {
|
|
||||||
this.setListAdapter(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface CountrySelectedListener {
|
|
||||||
public void countrySelected(String countryName, int countryCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FilterWatcher implements TextWatcher {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
if (getListAdapter() != null) {
|
|
||||||
((SimpleAdapter)getListAdapter()).getFilter().filter(s.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -164,25 +164,29 @@ public class DeviceListFragment extends ListFragment
|
|||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private void handleDisconnectDevice(final long deviceId) {
|
private void handleDisconnectDevice(final long deviceId) {
|
||||||
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
|
new ProgressDialogAsyncTask<Void, Void, Boolean>(getActivity(),
|
||||||
R.string.DeviceListActivity_unlinking_device_no_ellipsis,
|
R.string.DeviceListActivity_unlinking_device_no_ellipsis,
|
||||||
R.string.DeviceListActivity_unlinking_device)
|
R.string.DeviceListActivity_unlinking_device)
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(Void... params) {
|
protected Boolean doInBackground(Void... params) {
|
||||||
try {
|
try {
|
||||||
accountManager.removeDevice(deviceId);
|
accountManager.removeDevice(deviceId);
|
||||||
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
return false;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Void result) {
|
protected void onPostExecute(Boolean result) {
|
||||||
super.onPostExecute(result);
|
super.onPostExecute(result);
|
||||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
if (result) {
|
||||||
|
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle bundle, boolean ready) {
|
protected void onCreate(Bundle bundle, boolean ready) {
|
||||||
assert getSupportActionBar() != null;
|
|
||||||
getSupportActionBar().hide();
|
|
||||||
|
|
||||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||||
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
|
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
|
||||||
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
|
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
|
||||||
|
|||||||
@@ -1,322 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
|
|
||||||
import com.melnykov.fab.FloatingActionButton;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.IntroPagerAdapter.IntroPage;
|
|
||||||
import org.thoughtcrime.securesms.experienceupgrades.StickersIntroFragment;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationIds;
|
|
||||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ExperienceUpgradeActivity extends BaseActionBarActivity
|
|
||||||
implements TypingIndicatorIntroFragment.Controller,
|
|
||||||
LinkPreviewsIntroFragment.Controller,
|
|
||||||
StickersIntroFragment.Controller
|
|
||||||
{
|
|
||||||
private static final String TAG = ExperienceUpgradeActivity.class.getSimpleName();
|
|
||||||
private static final String DISMISS_ACTION = "org.thoughtcrime.securesms.ExperienceUpgradeActivity.DISMISS_ACTION";
|
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
|
||||||
|
|
||||||
private enum ExperienceUpgrade {
|
|
||||||
SIGNAL_REBRANDING(157,
|
|
||||||
new IntroPage(0xFF2090EA,
|
|
||||||
BasicIntroFragment.newInstance(R.drawable.splash_logo,
|
|
||||||
R.string.ExperienceUpgradeActivity_welcome_to_signal_dgaf,
|
|
||||||
R.string.ExperienceUpgradeActivity_textsecure_is_now_called_signal)),
|
|
||||||
R.string.ExperienceUpgradeActivity_welcome_to_signal_excited,
|
|
||||||
R.string.ExperienceUpgradeActivity_textsecure_is_now_signal,
|
|
||||||
R.string.ExperienceUpgradeActivity_textsecure_is_now_signal_long,
|
|
||||||
null,
|
|
||||||
false),
|
|
||||||
VIDEO_CALLS(245,
|
|
||||||
new IntroPage(0xFF2090EA,
|
|
||||||
BasicIntroFragment.newInstance(R.drawable.video_splash,
|
|
||||||
R.string.ExperienceUpgradeActivity_say_hello_to_video_calls,
|
|
||||||
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calls)),
|
|
||||||
R.string.ExperienceUpgradeActivity_say_hello_to_video_calls,
|
|
||||||
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calling,
|
|
||||||
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calling_long,
|
|
||||||
null,
|
|
||||||
false),
|
|
||||||
PROFILES(286,
|
|
||||||
new IntroPage(0xFF2090EA,
|
|
||||||
BasicIntroFragment.newInstance(R.drawable.profile_splash,
|
|
||||||
R.string.ExperienceUpgradeActivity_ready_for_your_closeup,
|
|
||||||
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal)),
|
|
||||||
R.string.ExperienceUpgradeActivity_signal_profiles_are_here,
|
|
||||||
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
|
|
||||||
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
|
|
||||||
EditProfileActivity.class,
|
|
||||||
false),
|
|
||||||
READ_RECEIPTS(299,
|
|
||||||
new IntroPage(0xFF2090EA,
|
|
||||||
ReadReceiptsIntroFragment.newInstance()),
|
|
||||||
R.string.experience_upgrade_preference_fragment__read_receipts_are_here,
|
|
||||||
R.string.experience_upgrade_preference_fragment__optionally_see_and_share_when_messages_have_been_read,
|
|
||||||
R.string.experience_upgrade_preference_fragment__optionally_see_and_share_when_messages_have_been_read,
|
|
||||||
null,
|
|
||||||
false),
|
|
||||||
TYPING_INDICATORS(432,
|
|
||||||
new IntroPage(0xFF2090EA,
|
|
||||||
TypingIndicatorIntroFragment.newInstance()),
|
|
||||||
R.string.ExperienceUpgradeActivity_introducing_typing_indicators,
|
|
||||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
|
||||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
|
||||||
null,
|
|
||||||
true),
|
|
||||||
LINK_PREVIEWS(449,
|
|
||||||
new IntroPage(0xFF2090EA, LinkPreviewsIntroFragment.newInstance()),
|
|
||||||
R.string.ExperienceUpgradeActivity_introducing_link_previews,
|
|
||||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
|
||||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
|
||||||
null,
|
|
||||||
true),
|
|
||||||
STICKERS(580,
|
|
||||||
new IntroPage(0xFF2090EA, StickersIntroFragment.newInstance()),
|
|
||||||
R.string.ExperienceUpgradeActivity_introducing_stickers,
|
|
||||||
R.string.ExperienceUpgradeActivity_why_use_words_when_you_can_use_stickers,
|
|
||||||
R.string.ExperienceUpgradeActivity_why_use_words_when_you_can_use_stickers,
|
|
||||||
null,
|
|
||||||
true);
|
|
||||||
|
|
||||||
private int version;
|
|
||||||
private List<IntroPage> pages;
|
|
||||||
private @StringRes int notificationTitle;
|
|
||||||
private @StringRes int notificationText;
|
|
||||||
private @StringRes int notificationBigText;
|
|
||||||
private @Nullable Class nextIntent;
|
|
||||||
private boolean handlesNavigation;
|
|
||||||
|
|
||||||
ExperienceUpgrade(int version,
|
|
||||||
@NonNull List<IntroPage> pages,
|
|
||||||
@StringRes int notificationTitle,
|
|
||||||
@StringRes int notificationText,
|
|
||||||
@StringRes int notificationBigText,
|
|
||||||
@Nullable Class nextIntent,
|
|
||||||
boolean handlesNavigation)
|
|
||||||
{
|
|
||||||
this.version = version;
|
|
||||||
this.pages = pages;
|
|
||||||
this.notificationTitle = notificationTitle;
|
|
||||||
this.notificationText = notificationText;
|
|
||||||
this.notificationBigText = notificationBigText;
|
|
||||||
this.nextIntent = nextIntent;
|
|
||||||
this.handlesNavigation = handlesNavigation;
|
|
||||||
}
|
|
||||||
|
|
||||||
ExperienceUpgrade(int version,
|
|
||||||
@NonNull IntroPage page,
|
|
||||||
@StringRes int notificationTitle,
|
|
||||||
@StringRes int notificationText,
|
|
||||||
@StringRes int notificationBigText,
|
|
||||||
@Nullable Class nextIntent,
|
|
||||||
boolean handlesNavigation)
|
|
||||||
{
|
|
||||||
this(version, Collections.singletonList(page), notificationTitle, notificationText, notificationBigText, nextIntent, handlesNavigation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getVersion() {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<IntroPage> getPages() {
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IntroPage getPage(int i) {
|
|
||||||
return pages.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getNotificationTitle() {
|
|
||||||
return notificationTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getNotificationText() {
|
|
||||||
return notificationText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getNotificationBigText() {
|
|
||||||
return notificationBigText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean handlesNavigation() {
|
|
||||||
return handlesNavigation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
dynamicTheme.onCreate(this);
|
|
||||||
|
|
||||||
final Optional<ExperienceUpgrade> upgrade = getExperienceUpgrade(this);
|
|
||||||
if (!upgrade.isPresent()) {
|
|
||||||
onContinue(upgrade);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.experience_upgrade_activity);
|
|
||||||
final ViewPager pager = ViewUtil.findById(this, R.id.pager);
|
|
||||||
final FloatingActionButton fab = ViewUtil.findById(this, R.id.fab);
|
|
||||||
|
|
||||||
pager.setAdapter(new IntroPagerAdapter(getSupportFragmentManager(), upgrade.get().getPages()));
|
|
||||||
|
|
||||||
if (upgrade.get().handlesNavigation()) {
|
|
||||||
fab.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
fab.setVisibility(View.VISIBLE);
|
|
||||||
fab.setOnClickListener(v -> onContinue(upgrade));
|
|
||||||
}
|
|
||||||
|
|
||||||
getWindow().setBackgroundDrawable(new ColorDrawable(upgrade.get().getPage(0).backgroundColor));
|
|
||||||
ServiceUtil.getNotificationManager(this).cancel(NotificationIds.EXPERIENCE_UPGRADE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
dynamicTheme.onResume(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onContinue(Optional<ExperienceUpgrade> seenUpgrade) {
|
|
||||||
ServiceUtil.getNotificationManager(this).cancel(NotificationIds.EXPERIENCE_UPGRADE);
|
|
||||||
int latestVersion = seenUpgrade.isPresent() ? seenUpgrade.get().getVersion()
|
|
||||||
: Util.getCanonicalVersionCode();
|
|
||||||
TextSecurePreferences.setLastExperienceVersionCode(this, latestVersion);
|
|
||||||
if (seenUpgrade.isPresent() && seenUpgrade.get().nextIntent != null) {
|
|
||||||
Intent intent = new Intent(this, seenUpgrade.get().nextIntent);
|
|
||||||
// TODO [greyson] Navigation
|
|
||||||
Intent nextIntent = new Intent(this, MainActivity.class);
|
|
||||||
intent.putExtra("next_intent", nextIntent);
|
|
||||||
startActivity(intent);
|
|
||||||
} else {
|
|
||||||
startActivity(getIntent().getParcelableExtra("next_intent"));
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isUpdate(Context context) {
|
|
||||||
return getExperienceUpgrade(context).isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Optional<ExperienceUpgrade> getExperienceUpgrade(Context context) {
|
|
||||||
final int currentVersionCode = Util.getCanonicalVersionCode();
|
|
||||||
final int lastSeenVersion = TextSecurePreferences.getLastExperienceVersionCode(context);
|
|
||||||
Log.i(TAG, "getExperienceUpgrade(" + lastSeenVersion + ")");
|
|
||||||
|
|
||||||
if (lastSeenVersion >= currentVersionCode) {
|
|
||||||
TextSecurePreferences.setLastExperienceVersionCode(context, currentVersionCode);
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<ExperienceUpgrade> eligibleUpgrade = Optional.absent();
|
|
||||||
for (ExperienceUpgrade upgrade : ExperienceUpgrade.values()) {
|
|
||||||
if (lastSeenVersion < upgrade.getVersion()) eligibleUpgrade = Optional.of(upgrade);
|
|
||||||
}
|
|
||||||
|
|
||||||
return eligibleUpgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTypingIndicatorsFinished() {
|
|
||||||
onContinue(Optional.of(ExperienceUpgrade.TYPING_INDICATORS));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLinkPreviewsFinished() {
|
|
||||||
onContinue(Optional.of(ExperienceUpgrade.LINK_PREVIEWS));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStickersFinished() {
|
|
||||||
onContinue(Optional.of(ExperienceUpgrade.STICKERS));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AppUpgradeReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction()) &&
|
|
||||||
intent.getData().getSchemeSpecificPart().equals(context.getPackageName()))
|
|
||||||
{
|
|
||||||
if (TextSecurePreferences.getLastExperienceVersionCode(context) < 339 &&
|
|
||||||
!TextSecurePreferences.isPasswordDisabled(context))
|
|
||||||
{
|
|
||||||
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.OTHER)
|
|
||||||
.setSmallIcon(R.drawable.icon_notification)
|
|
||||||
.setColor(context.getResources().getColor(R.color.signal_primary))
|
|
||||||
.setContentTitle(context.getString(R.string.ExperienceUpgradeActivity_unlock_to_complete_update))
|
|
||||||
.setContentText(context.getString(R.string.ExperienceUpgradeActivity_please_unlock_signal_to_complete_update))
|
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.ExperienceUpgradeActivity_please_unlock_signal_to_complete_update)))
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0,
|
|
||||||
context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
ServiceUtil.getNotificationManager(context).notify(NotificationIds.EXPERIENCE_UPGRADE, notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<ExperienceUpgrade> experienceUpgrade = getExperienceUpgrade(context);
|
|
||||||
|
|
||||||
if (!experienceUpgrade.isPresent()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (experienceUpgrade.get().getVersion() == TextSecurePreferences.getExperienceDismissedVersionCode(context)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent targetIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
|
|
||||||
Intent dismissIntent = new Intent(context, AppUpgradeReceiver.class);
|
|
||||||
dismissIntent.setAction(DISMISS_ACTION);
|
|
||||||
|
|
||||||
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.OTHER)
|
|
||||||
.setSmallIcon(R.drawable.icon_notification)
|
|
||||||
.setColor(context.getResources().getColor(R.color.signal_primary))
|
|
||||||
.setContentTitle(context.getString(experienceUpgrade.get().getNotificationTitle()))
|
|
||||||
.setContentText(context.getString(experienceUpgrade.get().getNotificationText()))
|
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(experienceUpgrade.get().getNotificationBigText())))
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0,
|
|
||||||
targetIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
|
||||||
|
|
||||||
.setDeleteIntent(PendingIntent.getBroadcast(context, 0,
|
|
||||||
dismissIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
|
||||||
.build();
|
|
||||||
ServiceUtil.getNotificationManager(context).notify(NotificationIds.EXPERIENCE_UPGRADE, notification);
|
|
||||||
} else if (DISMISS_ACTION.equals(intent.getAction())) {
|
|
||||||
TextSecurePreferences.setExperienceDismissedVersionCode(context, Util.getCanonicalVersionCode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,20 +18,14 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@@ -42,6 +36,10 @@ import android.widget.ListView;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.request.target.SimpleTarget;
|
import com.bumptech.glide.request.target.SimpleTarget;
|
||||||
import com.bumptech.glide.request.transition.Transition;
|
import com.bumptech.glide.request.transition.Transition;
|
||||||
@@ -52,28 +50,35 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
|||||||
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
|
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
|
||||||
|
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
|
||||||
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
|
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
|
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
|
||||||
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
|
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -81,7 +86,7 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to create and update groups
|
* Activity to create and update {@link GroupId.V1} groups
|
||||||
*
|
*
|
||||||
* @author Jake McGinty
|
* @author Jake McGinty
|
||||||
*/
|
*/
|
||||||
@@ -92,27 +97,31 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
private final static String TAG = GroupCreateActivity.class.getSimpleName();
|
private final static String TAG = GroupCreateActivity.class.getSimpleName();
|
||||||
|
|
||||||
public static final String GROUP_ID_EXTRA = "group_id";
|
private static final String GROUP_ID_EXTRA = "group_id";
|
||||||
public static final String GROUP_THREAD_EXTRA = "group_thread";
|
private static final String GROUP_THREAD_EXTRA = "group_thread";
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
|
||||||
|
|
||||||
private static final int PICK_CONTACT = 1;
|
private static final short REQUEST_CODE_SELECT_AVATAR = 26165;
|
||||||
public static final int AVATAR_SIZE = 210;
|
private static final int PICK_CONTACT = 1;
|
||||||
|
|
||||||
private EditText groupName;
|
private EditText groupName;
|
||||||
private ListView lv;
|
private ListView listView;
|
||||||
private ImageView avatar;
|
private ImageView avatar;
|
||||||
private TextView creatingText;
|
private TextView creatingText;
|
||||||
private Bitmap avatarBmp;
|
private Bitmap avatarBmp;
|
||||||
|
|
||||||
@NonNull private Optional<GroupData> groupToUpdate = Optional.absent();
|
@NonNull private Optional<GroupData> groupToUpdate = Optional.absent();
|
||||||
|
|
||||||
|
public static Intent newEditGroupIntent(@NonNull Context context, @NonNull GroupId.V1 groupId) {
|
||||||
|
Intent intent = new Intent(context, GroupCreateActivity.class);
|
||||||
|
intent.putExtra(GroupCreateActivity.GROUP_ID_EXTRA, groupId.toString());
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPreCreate() {
|
protected void onPreCreate() {
|
||||||
dynamicTheme.onCreate(this);
|
dynamicTheme.onCreate(this);
|
||||||
dynamicLanguage.onCreate(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -128,7 +137,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
dynamicTheme.onResume(this);
|
dynamicTheme.onResume(this);
|
||||||
dynamicLanguage.onResume(this);
|
|
||||||
updateViewState();
|
updateViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,27 +193,42 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeResources() {
|
private void initializeResources() {
|
||||||
RecipientsEditor recipientsEditor = ViewUtil.findById(this, R.id.recipients_text);
|
RecipientsEditor recipientsEditor = findViewById(R.id.recipients_text);
|
||||||
PushRecipientsPanel recipientsPanel = ViewUtil.findById(this, R.id.recipients);
|
PushRecipientsPanel recipientsPanel = findViewById(R.id.recipients);
|
||||||
lv = ViewUtil.findById(this, R.id.selected_contacts_list);
|
|
||||||
avatar = ViewUtil.findById(this, R.id.avatar);
|
listView = findViewById(R.id.selected_contacts_list);
|
||||||
groupName = ViewUtil.findById(this, R.id.group_name);
|
avatar = findViewById(R.id.avatar);
|
||||||
creatingText = ViewUtil.findById(this, R.id.creating_group_text);
|
groupName = findViewById(R.id.group_name);
|
||||||
|
creatingText = findViewById(R.id.creating_group_text);
|
||||||
|
|
||||||
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(this);
|
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(this);
|
||||||
adapter.setOnRecipientDeletedListener(this);
|
adapter.setOnRecipientDeletedListener(this);
|
||||||
lv.setAdapter(adapter);
|
listView.setAdapter(adapter);
|
||||||
|
|
||||||
recipientsEditor.setHint(R.string.recipients_panel__add_members);
|
recipientsEditor.setHint(R.string.recipients_panel__add_members);
|
||||||
recipientsPanel.setPanelChangeListener(this);
|
recipientsPanel.setPanelChangeListener(this);
|
||||||
|
|
||||||
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
|
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
|
||||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_outline_40, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this)));
|
|
||||||
avatar.setOnClickListener(view -> AvatarSelection.startAvatarSelection(this, false, false));
|
avatar.setImageDrawable(getDefaultGroupAvatar());
|
||||||
|
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR, true).show(getSupportFragmentManager(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Drawable getDefaultGroupAvatar() {
|
||||||
|
return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeExistingGroup() {
|
private void initializeExistingGroup() {
|
||||||
final String groupId = getIntent().getStringExtra(GROUP_ID_EXTRA);
|
final GroupId groupId = GroupId.parseNullableOrThrow(getIntent().getStringExtra(GROUP_ID_EXTRA));
|
||||||
|
|
||||||
if (groupId != null) {
|
if (groupId != null) {
|
||||||
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupId);
|
GroupId.V1 groupIdV1 = groupId.requireV1();
|
||||||
|
|
||||||
|
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupIdV1);
|
||||||
|
|
||||||
|
if (FeatureFlags.newGroupUI()) {
|
||||||
|
avatar.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(this, groupIdV1)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,8 +283,8 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleGroupUpdate() {
|
private void handleGroupUpdate() {
|
||||||
new UpdateSignalGroupTask(this, groupToUpdate.get().id, avatarBmp,
|
new UpdateSignalGroupV1Task(this, groupToUpdate.get().id, avatarBmp,
|
||||||
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleOpenConversation(long threadId, Recipient recipient) {
|
private void handleOpenConversation(long threadId, Recipient recipient) {
|
||||||
@@ -274,7 +297,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SelectedRecipientsAdapter getAdapter() {
|
private SelectedRecipientsAdapter getAdapter() {
|
||||||
return (SelectedRecipientsAdapter)lv.getAdapter();
|
return (SelectedRecipientsAdapter) listView.getAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable String getGroupName() {
|
private @Nullable String getGroupName() {
|
||||||
@@ -284,7 +307,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||||||
super.onActivityResult(reqCode, resultCode, data);
|
super.onActivityResult(reqCode, resultCode, data);
|
||||||
Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped"));
|
|
||||||
|
|
||||||
if (data == null || resultCode != Activity.RESULT_OK)
|
if (data == null || resultCode != Activity.RESULT_OK)
|
||||||
return;
|
return;
|
||||||
@@ -299,23 +321,27 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case REQUEST_CODE_SELECT_AVATAR:
|
||||||
|
if (data.getBooleanExtra("delete", false)) {
|
||||||
|
avatarBmp = null;
|
||||||
|
avatar.setImageDrawable(getDefaultGroupAvatar());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
|
||||||
|
final DecryptableUri decryptableUri = new DecryptableUri(result.getUri());
|
||||||
|
|
||||||
case AvatarSelection.REQUEST_CODE_AVATAR:
|
|
||||||
AvatarSelection.circularCropImage(this, data.getData(), outputFile, R.string.CropImageActivity_group_avatar);
|
|
||||||
break;
|
|
||||||
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
|
|
||||||
final Uri resultUri = AvatarSelection.getResultUri(data);
|
|
||||||
GlideApp.with(this)
|
GlideApp.with(this)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(resultUri)
|
.load(decryptableUri)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.override(AVATAR_SIZE, AVATAR_SIZE)
|
.override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
|
||||||
.into(new SimpleTarget<Bitmap>() {
|
.into(new SimpleTarget<Bitmap>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||||
setAvatar(resultUri, resource);
|
setAvatar(decryptableUri, resource);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -352,7 +378,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
memberAddresses.add(Recipient.self().getId());
|
memberAddresses.add(Recipient.self().getId());
|
||||||
|
|
||||||
String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
|
GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses);
|
||||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
|
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
|
||||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||||
@@ -416,7 +442,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
||||||
return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false));
|
return Optional.of(GroupManager.createGroupV1(activity, members, BitmapUtil.toByteArray(avatar), name, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -433,11 +459,11 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class UpdateSignalGroupTask extends SignalGroupTask {
|
private static class UpdateSignalGroupV1Task extends SignalGroupTask {
|
||||||
private String groupId;
|
private final GroupId.V1 groupId;
|
||||||
|
|
||||||
public UpdateSignalGroupTask(GroupCreateActivity activity, String groupId,
|
UpdateSignalGroupV1Task(GroupCreateActivity activity, GroupId.V1 groupId,
|
||||||
Bitmap avatar, String name, Set<Recipient> members)
|
Bitmap avatar, String name, Set<Recipient> members)
|
||||||
{
|
{
|
||||||
super(activity, avatar, name, members);
|
super(activity, avatar, name, members);
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
@@ -445,11 +471,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
||||||
try {
|
return Optional.fromNullable(GroupManager.updateGroup(activity, groupId, members, BitmapUtil.toByteArray(avatar), name));
|
||||||
return Optional.of(GroupManager.updateGroup(activity, groupId, members, avatar, name));
|
|
||||||
} catch (InvalidNumberException e) {
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -458,7 +480,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
if (!activity.isFinishing()) {
|
if (!activity.isFinishing()) {
|
||||||
Intent intent = activity.getIntent();
|
Intent intent = activity.getIntent();
|
||||||
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
|
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
|
||||||
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId());
|
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId().toString());
|
||||||
activity.setResult(RESULT_OK, intent);
|
activity.setResult(RESULT_OK, intent);
|
||||||
activity.finish();
|
activity.finish();
|
||||||
}
|
}
|
||||||
@@ -525,7 +547,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<String,Void,Optional<GroupData>> {
|
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<GroupId.V1, Void, Optional<GroupData>> {
|
||||||
private GroupCreateActivity activity;
|
private GroupCreateActivity activity;
|
||||||
|
|
||||||
public FillExistingGroupInfoAsyncTask(GroupCreateActivity activity) {
|
public FillExistingGroupInfoAsyncTask(GroupCreateActivity activity) {
|
||||||
@@ -536,18 +558,24 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<GroupData> doInBackground(String... groupIds) {
|
protected Optional<GroupData> doInBackground(GroupId.V1... groupIds) {
|
||||||
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
|
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
|
||||||
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], false);
|
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||||
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
|
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
|
||||||
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
|
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
|
||||||
existingContacts.addAll(recipients);
|
existingContacts.addAll(recipients);
|
||||||
|
|
||||||
if (group.isPresent()) {
|
if (group.isPresent()) {
|
||||||
|
Bitmap avatar = null;
|
||||||
|
try {
|
||||||
|
avatar = BitmapFactory.decodeStream(AvatarHelper.getAvatar(getContext(), group.get().getRecipientId()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "Failed to read avatar.");
|
||||||
|
}
|
||||||
return Optional.of(new GroupData(groupIds[0],
|
return Optional.of(new GroupData(groupIds[0],
|
||||||
existingContacts,
|
existingContacts,
|
||||||
BitmapUtil.fromByteArray(group.get().getAvatar()),
|
avatar,
|
||||||
group.get().getAvatar(),
|
BitmapUtil.toByteArray(avatar),
|
||||||
group.get().getTitle()));
|
group.get().getTitle()));
|
||||||
} else {
|
} else {
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
@@ -567,7 +595,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(activity, group.get().recipients);
|
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(activity, group.get().recipients);
|
||||||
adapter.setOnRecipientDeletedListener(activity);
|
adapter.setOnRecipientDeletedListener(activity);
|
||||||
activity.lv.setAdapter(adapter);
|
activity.listView.setAdapter(adapter);
|
||||||
activity.updateViewState();
|
activity.updateViewState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,13 +612,13 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class GroupData {
|
private static class GroupData {
|
||||||
String id;
|
GroupId.V1 id;
|
||||||
Set<Recipient> recipients;
|
Set<Recipient> recipients;
|
||||||
Bitmap avatarBmp;
|
Bitmap avatarBmp;
|
||||||
byte[] avatarBytes;
|
byte[] avatarBytes;
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
|
GroupData(GroupId.V1 id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.recipients = recipients;
|
this.recipients = recipients;
|
||||||
this.avatarBmp = avatarBmp;
|
this.avatarBmp = avatarBmp;
|
||||||
|
|||||||
@@ -1,131 +1,57 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.content.Context;
|
import androidx.annotation.NonNull;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
|
public final class GroupMembersDialog {
|
||||||
|
|
||||||
private static final String TAG = GroupMembersDialog.class.getSimpleName();
|
private final FragmentActivity fragmentActivity;
|
||||||
|
private final Recipient groupRecipient;
|
||||||
|
|
||||||
private final Recipient recipient;
|
public GroupMembersDialog(@NonNull FragmentActivity activity,
|
||||||
private final Context context;
|
@NonNull Recipient groupRecipient)
|
||||||
|
{
|
||||||
public GroupMembersDialog(Context context, Recipient recipient) {
|
this.fragmentActivity = activity;
|
||||||
this.recipient = recipient;
|
this.groupRecipient = groupRecipient;
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPreExecute() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Recipient> doInBackground(Void... params) {
|
|
||||||
return DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPostExecute(List<Recipient> members) {
|
|
||||||
GroupMembers groupMembers = new GroupMembers(members);
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
|
||||||
builder.setTitle(R.string.ConversationActivity_group_members);
|
|
||||||
builder.setIconAttribute(R.attr.group_members_dialog_icon);
|
|
||||||
builder.setCancelable(true);
|
|
||||||
builder.setItems(groupMembers.getRecipientStrings(), new GroupMembersOnClickListener(context, groupMembers));
|
|
||||||
builder.setPositiveButton(android.R.string.ok, null);
|
|
||||||
builder.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void display() {
|
public void display() {
|
||||||
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
|
||||||
|
.setTitle(R.string.ConversationActivity_group_members)
|
||||||
|
.setIconAttribute(R.attr.group_members_dialog_icon)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setView(R.layout.dialog_group_members)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
|
||||||
|
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
||||||
|
|
||||||
|
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
|
||||||
|
LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers();
|
||||||
|
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
fullMembers.observe(fragmentActivity, memberListView::setMembers);
|
||||||
|
|
||||||
|
dialog.setOnDismissListener(d -> fullMembers.removeObservers(fragmentActivity));
|
||||||
|
|
||||||
|
memberListView.setRecipientClickListener(recipient -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
contactClick(recipient);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class GroupMembersOnClickListener implements DialogInterface.OnClickListener {
|
private void contactClick(@NonNull Recipient recipient) {
|
||||||
private final GroupMembers groupMembers;
|
RecipientBottomSheetDialogFragment.create(recipient.getId(), groupRecipient.requireGroupId())
|
||||||
private final Context context;
|
.show(fragmentActivity.getSupportFragmentManager(), "BOTTOM");
|
||||||
|
|
||||||
public GroupMembersOnClickListener(Context context, GroupMembers members) {
|
|
||||||
this.context = context;
|
|
||||||
this.groupMembers = members;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialogInterface, int item) {
|
|
||||||
Recipient recipient = groupMembers.get(item);
|
|
||||||
|
|
||||||
if (recipient.getContactUri() != null) {
|
|
||||||
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
|
|
||||||
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId());
|
|
||||||
|
|
||||||
context.startActivity(intent);
|
|
||||||
} else {
|
|
||||||
context.startActivity(RecipientExporter.export(recipient).asAddContactIntent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a List of Recipient (just like @class Recipients),
|
|
||||||
* but with focus on the order of the Recipients.
|
|
||||||
* So that the order of the RecipientStrings[] matches
|
|
||||||
* the internal order.
|
|
||||||
*
|
|
||||||
* @author Christoph Haefner
|
|
||||||
*/
|
|
||||||
private class GroupMembers {
|
|
||||||
private final String TAG = GroupMembers.class.getSimpleName();
|
|
||||||
|
|
||||||
private final LinkedList<Recipient> members = new LinkedList<>();
|
|
||||||
|
|
||||||
public GroupMembers(List<Recipient> recipients) {
|
|
||||||
for (Recipient recipient : recipients) {
|
|
||||||
if (recipient.isLocalNumber()) {
|
|
||||||
members.push(recipient);
|
|
||||||
} else {
|
|
||||||
members.add(recipient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getRecipientStrings() {
|
|
||||||
List<String> recipientStrings = new LinkedList<>();
|
|
||||||
|
|
||||||
for (Recipient recipient : members) {
|
|
||||||
if (recipient.isLocalNumber()) {
|
|
||||||
recipientStrings.add(context.getString(R.string.GroupMembersDialog_me));
|
|
||||||
} else {
|
|
||||||
String name = getRecipientName(recipient);
|
|
||||||
recipientStrings.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return recipientStrings.toArray(new String[members.size()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getRecipientName(Recipient recipient) {
|
|
||||||
if (FeatureFlags.profileDisplay()) return recipient.getDisplayName(context);
|
|
||||||
|
|
||||||
String name = recipient.toShortString(context);
|
|
||||||
|
|
||||||
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
|
|
||||||
name += " ~" + recipient.getProfileName().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Recipient get(int index) {
|
|
||||||
return members.get(index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class IntroPagerAdapter extends FragmentStatePagerAdapter {
|
|
||||||
|
|
||||||
public static class IntroPage {
|
|
||||||
final int backgroundColor;
|
|
||||||
final Fragment fragment;
|
|
||||||
|
|
||||||
public IntroPage(int backgroundColor, Fragment fragment) {
|
|
||||||
this.backgroundColor = backgroundColor;
|
|
||||||
this.fragment = fragment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<IntroPage> pages;
|
|
||||||
|
|
||||||
public IntroPagerAdapter(FragmentManager fm, List<IntroPage> pages) {
|
|
||||||
super(fm);
|
|
||||||
this.pages = pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Fragment getItem(int i) {
|
|
||||||
IntroPage page = pages.get(i);
|
|
||||||
return page.fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return pages.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,8 +22,6 @@ import androidx.appcompat.widget.Toolbar;
|
|||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener;
|
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||||
@@ -42,6 +40,7 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
|||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||||
@@ -135,14 +134,15 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
|||||||
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
|
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
|
||||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
||||||
contactsFragment.getSelectedContacts()
|
contactsFragment.getSelectedContacts()
|
||||||
.toArray(new SelectedContact[contactsFragment.getSelectedContacts().size()]));
|
.toArray(new SelectedContact[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSmsButtonText() {
|
private void updateSmsButtonText() {
|
||||||
|
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||||
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
|
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
|
||||||
contactsFragment.getSelectedContacts().size(),
|
selectedContacts.size(),
|
||||||
contactsFragment.getSelectedContacts().size()));
|
selectedContacts.size()));
|
||||||
smsSendButton.setEnabled(!contactsFragment.getSelectedContacts().isEmpty());
|
smsSendButton.setEnabled(!selectedContacts.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void onBackPressed() {
|
@Override public void onBackPressed() {
|
||||||
@@ -175,17 +175,17 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setPrimaryColorsToolbarForSms() {
|
private void setPrimaryColorsToolbarForSms() {
|
||||||
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.signal_primary));
|
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||||
primaryToolbar.getNavigationIcon().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_subtitle_color), PorterDuff.Mode.SRC_IN);
|
primaryToolbar.getNavigationIcon().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_subtitle_color), PorterDuff.Mode.SRC_IN);
|
||||||
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.signal_primary));
|
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||||
WindowUtil.clearLightStatusBar(getWindow());
|
WindowUtil.clearLightStatusBar(getWindow());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 27) {
|
if (Build.VERSION.SDK_INT >= 27) {
|
||||||
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.signal_primary));
|
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||||
WindowUtil.clearLightNavigationBar(getWindow());
|
WindowUtil.clearLightNavigationBar(getWindow());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
|
|
||||||
public class LinkPreviewsIntroFragment extends Fragment {
|
|
||||||
|
|
||||||
private Controller controller;
|
|
||||||
|
|
||||||
public static LinkPreviewsIntroFragment newInstance() {
|
|
||||||
LinkPreviewsIntroFragment fragment = new LinkPreviewsIntroFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LinkPreviewsIntroFragment() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
|
|
||||||
if (!(getActivity() instanceof Controller)) {
|
|
||||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
|
||||||
}
|
|
||||||
|
|
||||||
controller = (Controller) getActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.experience_upgrade_link_previews_fragment, container, false);
|
|
||||||
|
|
||||||
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
|
|
||||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
|
||||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
|
||||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
|
||||||
TextSecurePreferences.isLinkPreviewsEnabled(requireContext())));
|
|
||||||
controller.onLinkPreviewsFinished();
|
|
||||||
});
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Controller {
|
|
||||||
void onLinkPreviewsFinished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.logsubmit.SubmitLogFragment;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity for submitting logcat logs to a pastebin service.
|
|
||||||
*/
|
|
||||||
public class LogSubmitActivity extends BaseActionBarActivity implements SubmitLogFragment.OnLogSubmittedListener {
|
|
||||||
|
|
||||||
private static final String TAG = LogSubmitActivity.class.getSimpleName();
|
|
||||||
private DynamicTheme dynamicTheme = new DynamicTheme();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle icicle) {
|
|
||||||
dynamicTheme.onCreate(this);
|
|
||||||
super.onCreate(icicle);
|
|
||||||
setContentView(R.layout.log_submit_activity);
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
SubmitLogFragment fragment = SubmitLogFragment.newInstance();
|
|
||||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
|
||||||
transaction.replace(R.id.fragment_container, fragment);
|
|
||||||
transaction.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
dynamicTheme.onResume(this);
|
|
||||||
super.onResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
super.onOptionsItemSelected(item);
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.home:
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSuccess() {
|
|
||||||
Toast.makeText(getApplicationContext(), R.string.log_submit_activity__thanks, Toast.LENGTH_LONG).show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure() {
|
|
||||||
Toast.makeText(getApplicationContext(), R.string.log_submit_activity__log_fetch_failed, Toast.LENGTH_LONG).show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCancel() {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startActivity(Intent intent) {
|
|
||||||
try {
|
|
||||||
super.startActivity(intent);
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
Toast.makeText(this, R.string.log_submit_activity__no_browser_installed, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ import androidx.fragment.app.FragmentManager;
|
|||||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
|
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
|
||||||
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
|
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
|
||||||
@@ -55,8 +56,8 @@ public class MainNavigator {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, long lastSeen, int startingPosition) {
|
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition) {
|
||||||
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, lastSeen, startingPosition);
|
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, startingPosition);
|
||||||
|
|
||||||
activity.startActivity(intent);
|
activity.startActivity(intent);
|
||||||
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
|
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
|
||||||
@@ -77,8 +78,7 @@ public class MainNavigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void goToGroupCreation() {
|
public void goToGroupCreation() {
|
||||||
Intent intent = new Intent(activity, GroupCreateActivity.class);
|
activity.startActivity(CreateGroupActivity.newIntent(activity));
|
||||||
activity.startActivity(intent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void goToInvite() {
|
public void goToInvite() {
|
||||||
|
|||||||
@@ -117,6 +117,20 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||||||
private boolean showThread;
|
private boolean showThread;
|
||||||
private MediaDatabase.Sorting sorting;
|
private MediaDatabase.Sorting sorting;
|
||||||
|
|
||||||
|
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
|
||||||
|
@NonNull MediaRecord mediaRecord,
|
||||||
|
boolean leftIsRecent)
|
||||||
|
{
|
||||||
|
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||||
|
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
|
||||||
|
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
||||||
|
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
|
||||||
|
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption());
|
||||||
|
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||||
|
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle bundle, boolean ready) {
|
protected void onCreate(Bundle bundle, boolean ready) {
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
|||||||
import androidx.loader.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
|
import android.os.Parcelable;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -51,7 +55,6 @@ import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
@@ -61,6 +64,7 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
@@ -131,13 +135,13 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
assert getSupportActionBar() != null;
|
assert getSupportActionBar() != null;
|
||||||
getSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
|
getSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
|
||||||
|
|
||||||
MessageNotifier.setVisibleThread(threadId);
|
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
MessageNotifier.setVisibleThread(-1L);
|
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -268,7 +272,9 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
toFrom.setText(toFromRes);
|
toFrom.setText(toFromRes);
|
||||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
|
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
|
||||||
|
Parcelable state = recipientsList.onSaveInstanceState();
|
||||||
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
|
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
|
||||||
|
recipientsList.onRestoreInstanceState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
|
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
|
||||||
@@ -276,9 +282,9 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
if (messageRecord.isGroupAction()) {
|
if (messageRecord.isGroupAction()) {
|
||||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false);
|
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false);
|
||||||
} else if (messageRecord.isOutgoing()) {
|
} else if (messageRecord.isOutgoing()) {
|
||||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent, itemParent, false);
|
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent_multimedia, itemParent, false);
|
||||||
} else {
|
} else {
|
||||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received, itemParent, false);
|
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received_multimedia, itemParent, false);
|
||||||
}
|
}
|
||||||
itemParent.addView(conversationItem);
|
itemParent.addView(conversationItem);
|
||||||
}
|
}
|
||||||
@@ -368,7 +374,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
|
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
|
||||||
|
|
||||||
if (receiptInfoList.isEmpty()) {
|
if (receiptInfoList.isEmpty()) {
|
||||||
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), false);
|
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||||
|
|
||||||
for (Recipient recipient : group) {
|
for (Recipient recipient : group) {
|
||||||
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
|
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
|
||||||
@@ -438,8 +444,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onResendClicked(View v) {
|
private void onResendClicked(View v) {
|
||||||
MessageSender.resend(MessageDetailsActivity.this, messageRecord);
|
|
||||||
resendButton.setVisibility(View.GONE);
|
resendButton.setVisibility(View.GONE);
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(MessageDetailsActivity.this, messageRecord));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,23 +81,45 @@ public class MessageRecipientListItem extends RelativeLayout
|
|||||||
this.deliveryStatusView = findViewById(R.id.delivery_status);
|
this.deliveryStatusView = findViewById(R.id.delivery_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow();
|
||||||
|
observeMember();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetachedFromWindow() {
|
||||||
|
unsubscribeFromMember();
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
}
|
||||||
|
|
||||||
public void set(final GlideRequests glideRequests,
|
public void set(final GlideRequests glideRequests,
|
||||||
final MessageRecord record,
|
final MessageRecord record,
|
||||||
final RecipientDeliveryStatus member,
|
final RecipientDeliveryStatus member,
|
||||||
final boolean isPushGroup)
|
final boolean isPushGroup)
|
||||||
{
|
{
|
||||||
if (this.member != null) this.member.getRecipient().live().removeForeverObserver(this);
|
unsubscribeFromMember();
|
||||||
|
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.member = member;
|
this.member = member;
|
||||||
|
observeMember();
|
||||||
|
|
||||||
member.getRecipient().live().observeForever(this);
|
|
||||||
fromView.setText(member.getRecipient());
|
fromView.setText(member.getRecipient());
|
||||||
contactPhotoImage.setAvatar(glideRequests, member.getRecipient(), false);
|
contactPhotoImage.setAvatar(glideRequests, member.getRecipient(), false);
|
||||||
setIssueIndicators(record, isPushGroup);
|
setIssueIndicators(record, isPushGroup);
|
||||||
unidentifiedDeliveryIcon.setVisibility(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()) && member.isUnidentified() ? VISIBLE : GONE);
|
unidentifiedDeliveryIcon.setVisibility(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()) && member.isUnidentified() ? VISIBLE : GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void observeMember() {
|
||||||
|
if (isAttachedToWindow() && member != null && member.getRecipient() != null) {
|
||||||
|
member.getRecipient().live().observeForever(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unsubscribeFromMember() {
|
||||||
|
if (member != null && member.getRecipient() != null) member.getRecipient().live().removeForeverObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void setIssueIndicators(final MessageRecord record,
|
private void setIssueIndicators(final MessageRecord record,
|
||||||
final boolean isPushGroup)
|
final boolean isPushGroup)
|
||||||
{
|
{
|
||||||
@@ -162,7 +184,7 @@ public class MessageRecipientListItem extends RelativeLayout
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void unbind() {
|
public void unbind() {
|
||||||
if (this.member != null && this.member.getRecipient() != null) this.member.getRecipient().live().removeForeverObserver(this);
|
unsubscribeFromMember();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -23,6 +25,10 @@ public class MuteDialog extends AlertDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
|
public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
|
||||||
|
show(context, listener, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
||||||
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
||||||
@@ -43,6 +49,13 @@ public class MuteDialog extends AlertDialog {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (cancelListener != null) {
|
||||||
|
builder.setOnCancelListener(dialog -> {
|
||||||
|
cancelListener.run();
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
builder.show();
|
builder.show();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,32 +19,16 @@ package org.thoughtcrime.securesms;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity container for starting a new conversation.
|
* Activity container for starting a new conversation.
|
||||||
@@ -53,7 +37,7 @@ import java.util.UUID;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class NewConversationActivity extends ContactSelectionActivity
|
public class NewConversationActivity extends ContactSelectionActivity
|
||||||
implements ContactSelectionListFragment.InviteCallback
|
implements ContactSelectionListFragment.ListCallback
|
||||||
{
|
{
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -112,7 +96,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleCreateGroup() {
|
private void handleCreateGroup() {
|
||||||
startActivity(new Intent(this, GroupCreateActivity.class));
|
startActivity(CreateGroupActivity.newIntent(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleInvite() {
|
private void handleInvite() {
|
||||||
@@ -120,10 +104,10 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onPrepareOptionsPanel(View view, Menu menu) {
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||||
MenuInflater inflater = this.getMenuInflater();
|
|
||||||
menu.clear();
|
menu.clear();
|
||||||
inflater.inflate(R.menu.new_conversation_activity, menu);
|
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
|
||||||
|
|
||||||
super.onPrepareOptionsMenu(menu);
|
super.onPrepareOptionsMenu(menu);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -131,5 +115,12 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onInvite() {
|
public void onInvite() {
|
||||||
handleInvite();
|
handleInvite();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewGroup(boolean forceV1) {
|
||||||
|
handleCreateGroup();
|
||||||
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||||||
|
|
||||||
public class PassphraseChangeActivity extends PassphraseActivity {
|
public class PassphraseChangeActivity extends PassphraseActivity {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(PassphraseChangeActivity.class);
|
||||||
|
|
||||||
private DynamicTheme dynamicTheme = new DynamicTheme();
|
private DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||||
|
|
||||||
@@ -145,7 +147,7 @@ public class PassphraseChangeActivity extends PassphraseActivity {
|
|||||||
return masterSecret;
|
return masterSecret;
|
||||||
|
|
||||||
} catch (InvalidPassphraseException e) {
|
} catch (InvalidPassphraseException e) {
|
||||||
Log.w(PassphraseChangeActivity.class.getSimpleName(), e);
|
Log.w(TAG, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.components.AnimatingToggle;
|
|||||||
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
|
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||||
|
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
|
||||||
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
|
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
@@ -164,7 +165,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleLogSubmit() {
|
private void handleLogSubmit() {
|
||||||
Intent intent = new Intent(this, LogSubmitActivity.class);
|
Intent intent = new Intent(this, SubmitDebugLogActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||||||
EditorInfo.IME_ACTION_DONE);
|
EditorInfo.IME_ACTION_DONE);
|
||||||
|
|
||||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||||
|
|
||||||
lockScreenButton.setOnClickListener(v -> resumeScreenLock());
|
lockScreenButton.setOnClickListener(v -> resumeScreenLock());
|
||||||
}
|
}
|
||||||
@@ -357,7 +358,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||||||
handleAuthenticated();
|
handleAuthenticated();
|
||||||
|
|
||||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -380,7 +381,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(Animation animation) {
|
public void onAnimationEnd(Animation animation) {
|
||||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -16,16 +16,15 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
|
||||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.util.CensorshipUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -33,16 +32,17 @@ import java.util.Locale;
|
|||||||
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
|
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
|
||||||
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
|
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
|
||||||
|
|
||||||
public static final String LOCALE_EXTRA = "locale_extra";
|
public static final String LOCALE_EXTRA = "locale_extra";
|
||||||
|
public static final String NEXT_INTENT_EXTRA = "next_intent";
|
||||||
|
|
||||||
private static final int STATE_NORMAL = 0;
|
private static final int STATE_NORMAL = 0;
|
||||||
private static final int STATE_CREATE_PASSPHRASE = 1;
|
private static final int STATE_CREATE_PASSPHRASE = 1;
|
||||||
private static final int STATE_PROMPT_PASSPHRASE = 2;
|
private static final int STATE_PROMPT_PASSPHRASE = 2;
|
||||||
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
|
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
|
||||||
private static final int STATE_EXPERIENCE_UPGRADE = 4;
|
private static final int STATE_WELCOME_PUSH_SCREEN = 4;
|
||||||
private static final int STATE_WELCOME_PUSH_SCREEN = 5;
|
private static final int STATE_ENTER_SIGNAL_PIN = 5;
|
||||||
private static final int STATE_CREATE_PROFILE_NAME = 6;
|
private static final int STATE_CREATE_PROFILE_NAME = 6;
|
||||||
private static final int STATE_CREATE_KBS_PIN = 7;
|
private static final int STATE_CREATE_SIGNAL_PIN = 7;
|
||||||
|
|
||||||
private SignalServiceNetworkAccess networkAccess;
|
private SignalServiceNetworkAccess networkAccess;
|
||||||
private BroadcastReceiver clearKeyReceiver;
|
private BroadcastReceiver clearKeyReceiver;
|
||||||
@@ -157,8 +157,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||||||
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
|
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
|
||||||
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
|
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
|
||||||
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
|
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
|
||||||
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
|
case STATE_ENTER_SIGNAL_PIN: return getEnterSignalPinIntent();
|
||||||
case STATE_CREATE_KBS_PIN: return getCreateKbsPinIntent();
|
case STATE_CREATE_SIGNAL_PIN: return getCreateSignalPinIntent();
|
||||||
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
@@ -173,23 +173,23 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||||||
return STATE_UI_BLOCKING_UPGRADE;
|
return STATE_UI_BLOCKING_UPGRADE;
|
||||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||||
return STATE_WELCOME_PUSH_SCREEN;
|
return STATE_WELCOME_PUSH_SCREEN;
|
||||||
} else if (ExperienceUpgradeActivity.isUpdate(this)) {
|
} else if (SignalStore.storageServiceValues().needsAccountRestore()) {
|
||||||
return STATE_EXPERIENCE_UPGRADE;
|
return STATE_ENTER_SIGNAL_PIN;
|
||||||
} else if (userMustSetProfileName()) {
|
} else if (userMustSetProfileName()) {
|
||||||
return STATE_CREATE_PROFILE_NAME;
|
return STATE_CREATE_PROFILE_NAME;
|
||||||
} else if (userMustSetKbsPin()) {
|
} else if (userMustCreateSignalPin()) {
|
||||||
return STATE_CREATE_KBS_PIN;
|
return STATE_CREATE_SIGNAL_PIN;
|
||||||
} else {
|
} else {
|
||||||
return STATE_NORMAL;
|
return STATE_NORMAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean userMustSetKbsPin() {
|
private boolean userMustCreateSignalPin() {
|
||||||
return !SignalStore.registrationValues().isRegistrationComplete() && !PinUtil.userHasPin(this);
|
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean userMustSetProfileName() {
|
private boolean userMustSetProfileName() {
|
||||||
return !SignalStore.registrationValues().isRegistrationComplete() && TextSecurePreferences.getProfileName(this) == ProfileName.EMPTY;
|
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getCreatePassphraseIntent() {
|
private Intent getCreatePassphraseIntent() {
|
||||||
@@ -207,15 +207,15 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||||||
: getPushRegistrationIntent());
|
: getPushRegistrationIntent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getExperienceUpgradeIntent() {
|
|
||||||
return getRoutedIntent(ExperienceUpgradeActivity.class, getIntent());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Intent getPushRegistrationIntent() {
|
private Intent getPushRegistrationIntent() {
|
||||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getCreateKbsPinIntent() {
|
private Intent getEnterSignalPinIntent() {
|
||||||
|
return getRoutedIntent(PinRestoreActivity.class, getIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent getCreateSignalPinIntent() {
|
||||||
|
|
||||||
final Intent intent;
|
final Intent intent;
|
||||||
if (userMustSetProfileName()) {
|
if (userMustSetProfileName()) {
|
||||||
@@ -261,4 +261,12 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||||||
clearKeyReceiver = null;
|
clearKeyReceiver = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts an extra in {@code intent} so that {@code nextIntent} will be shown after it.
|
||||||
|
*/
|
||||||
|
public static @NonNull Intent chainIntent(@NonNull Intent intent, @NonNull Intent nextIntent) {
|
||||||
|
intent.putExtra(NEXT_INTENT_EXTRA, nextIntent);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,16 +45,24 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
|||||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
||||||
super.onCreate(icicle, ready);
|
super.onCreate(icicle, ready);
|
||||||
|
|
||||||
|
initializeToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void initializeToolbar() {
|
||||||
getToolbar().setNavigationIcon(R.drawable.ic_check_24);
|
getToolbar().setNavigationIcon(R.drawable.ic_check_24);
|
||||||
getToolbar().setNavigationOnClickListener(v -> {
|
getToolbar().setNavigationOnClickListener(v -> {
|
||||||
Intent resultIntent = getIntent();
|
onFinishedSelection();
|
||||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
|
||||||
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
|
|
||||||
|
|
||||||
resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
|
|
||||||
|
|
||||||
setResult(RESULT_OK, resultIntent);
|
|
||||||
finish();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected final void onFinishedSelection() {
|
||||||
|
Intent resultIntent = getIntent();
|
||||||
|
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||||
|
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
|
||||||
|
|
||||||
|
resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
|
||||||
|
|
||||||
|
setResult(RESULT_OK, resultIntent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.appcompat.widget.SwitchCompat;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
|
||||||
|
|
||||||
public class ReadReceiptsIntroFragment extends Fragment {
|
|
||||||
|
|
||||||
public static ReadReceiptsIntroFragment newInstance() {
|
|
||||||
ReadReceiptsIntroFragment fragment = new ReadReceiptsIntroFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadReceiptsIntroFragment() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View v = inflater.inflate(R.layout.experience_upgrade_preference_fragment, container, false);
|
|
||||||
SwitchCompat preference = ViewUtil.findById(v, R.id.preference);
|
|
||||||
|
|
||||||
preference.setChecked(TextSecurePreferences.isReadReceiptsEnabled(getContext()));
|
|
||||||
preference.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
||||||
TextSecurePreferences.setReadReceiptsEnabled(getContext(), isChecked);
|
|
||||||
ApplicationDependencies.getJobManager()
|
|
||||||
.add(new MultiDeviceConfigurationUpdateJob(isChecked,
|
|
||||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
|
||||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
|
||||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
|
||||||
});
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.media.Ringtone;
|
import android.media.Ringtone;
|
||||||
import android.media.RingtoneManager;
|
import android.media.RingtoneManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@@ -22,10 +22,10 @@ import android.view.ViewGroup;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
@@ -37,7 +37,11 @@ import androidx.preference.Preference;
|
|||||||
import androidx.preference.PreferenceCategory;
|
import androidx.preference.PreferenceCategory;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.bumptech.glide.load.engine.GlideException;
|
||||||
|
import com.bumptech.glide.request.RequestListener;
|
||||||
|
import com.bumptech.glide.request.target.Target;
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||||
@@ -48,9 +52,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
|||||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
@@ -72,9 +74,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.Dialogs;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||||
@@ -84,7 +84,6 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
|||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
@@ -95,8 +94,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
{
|
{
|
||||||
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
|
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
|
||||||
|
|
||||||
public static final String RECIPIENT_ID = "recipient_address";
|
public static final String RECIPIENT_ID = "recipient";
|
||||||
public static final String CAN_HAVE_SAFETY_NUMBER_EXTRA = "can_have_safety_number";
|
|
||||||
|
|
||||||
private static final String PREFERENCE_MUTED = "pref_key_recipient_mute";
|
private static final String PREFERENCE_MUTED = "pref_key_recipient_mute";
|
||||||
private static final String PREFERENCE_MESSAGE_TONE = "pref_key_recipient_ringtone";
|
private static final String PREFERENCE_MESSAGE_TONE = "pref_key_recipient_ringtone";
|
||||||
@@ -109,8 +107,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
private static final String PREFERENCE_ABOUT = "pref_key_number";
|
private static final String PREFERENCE_ABOUT = "pref_key_number";
|
||||||
private static final String PREFERENCE_CUSTOM_NOTIFICATIONS = "pref_key_recipient_custom_notifications";
|
private static final String PREFERENCE_CUSTOM_NOTIFICATIONS = "pref_key_recipient_custom_notifications";
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
|
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
|
||||||
|
|
||||||
private ImageView avatar;
|
private ImageView avatar;
|
||||||
private GlideRequests glideRequests;
|
private GlideRequests glideRequests;
|
||||||
@@ -119,10 +116,16 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
private ThreadPhotoRailView threadPhotoRailView;
|
private ThreadPhotoRailView threadPhotoRailView;
|
||||||
private CollapsingToolbarLayout toolbarLayout;
|
private CollapsingToolbarLayout toolbarLayout;
|
||||||
|
|
||||||
|
public static @NonNull Intent getLaunchIntent(@NonNull Context context, @NonNull RecipientId id) {
|
||||||
|
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
|
||||||
|
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, id);
|
||||||
|
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreCreate() {
|
public void onPreCreate() {
|
||||||
dynamicTheme.onCreate(this);
|
dynamicTheme.onCreate(this);
|
||||||
dynamicLanguage.onCreate(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -137,14 +140,13 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
setHeader(recipient.get());
|
setHeader(recipient.get());
|
||||||
recipient.observe(this, this::setHeader);
|
recipient.observe(this, this::setHeader);
|
||||||
|
|
||||||
getSupportLoaderManager().initLoader(0, null, this);
|
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
dynamicTheme.onResume(this);
|
dynamicTheme.onResume(this);
|
||||||
dynamicLanguage.onResume(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -166,31 +168,19 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
finish();
|
|
||||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeToolbar() {
|
private void initializeToolbar() {
|
||||||
this.toolbarLayout = ViewUtil.findById(this, R.id.collapsing_toolbar);
|
this.toolbarLayout = findViewById(R.id.collapsing_toolbar);
|
||||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
this.avatar = findViewById(R.id.avatar);
|
||||||
this.threadPhotoRailView = ViewUtil.findById(this, R.id.recent_photos);
|
this.threadPhotoRailView = findViewById(R.id.recent_photos);
|
||||||
this.threadPhotoRailLabel = ViewUtil.findById(this, R.id.rail_label);
|
this.threadPhotoRailLabel = findViewById(R.id.rail_label);
|
||||||
|
|
||||||
this.toolbarLayout.setExpandedTitleColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
this.toolbarLayout.setExpandedTitleColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||||
this.toolbarLayout.setCollapsedTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
this.toolbarLayout.setCollapsedTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||||
|
|
||||||
this.threadPhotoRailView.setListener(mediaRecord -> {
|
this.threadPhotoRailView.setListener(mediaRecord ->
|
||||||
Intent intent = new Intent(RecipientPreferenceActivity.this, MediaPreviewActivity.class);
|
startActivity(MediaPreviewActivity.intentFromMediaRecord(RecipientPreferenceActivity.this,
|
||||||
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
|
mediaRecord,
|
||||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR)));
|
||||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
|
|
||||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption());
|
|
||||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR);
|
|
||||||
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
|
|
||||||
startActivity(intent);
|
|
||||||
});
|
|
||||||
|
|
||||||
SimpleTask.run(
|
SimpleTask.run(
|
||||||
() -> DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipientId),
|
() -> DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipientId),
|
||||||
@@ -203,7 +193,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Toolbar toolbar = ViewUtil.findById(this, R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
getSupportActionBar().setLogo(null);
|
getSupportActionBar().setLogo(null);
|
||||||
@@ -220,7 +210,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setHeader(@NonNull Recipient recipient) {
|
private void setHeader(@NonNull Recipient recipient) {
|
||||||
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this)))
|
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||||
: recipient.getContactPhoto();
|
: recipient.getContactPhoto();
|
||||||
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||||
: recipient.getFallbackContactPhoto();
|
: recipient.getFallbackContactPhoto();
|
||||||
@@ -229,6 +219,20 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
.fallback(fallbackPhoto.asCallCard(this))
|
.fallback(fallbackPhoto.asCallCard(this))
|
||||||
.error(fallbackPhoto.asCallCard(this))
|
.error(fallbackPhoto.asCallCard(this))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
|
.addListener(new RequestListener<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||||
|
avatar.setOnClickListener(null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||||
|
avatar.setOnClickListener(v -> startActivity(AvatarPreviewActivity.intentFromRecipientId(RecipientPreferenceActivity.this, recipient.getId()),
|
||||||
|
AvatarPreviewActivity.createTransitionBundle(RecipientPreferenceActivity.this, avatar)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
.into(this.avatar);
|
.into(this.avatar);
|
||||||
|
|
||||||
if (contactPhoto == null) this.avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
if (contactPhoto == null) this.avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
||||||
@@ -237,13 +241,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
this.avatar.setBackgroundColor(recipient.getColor().toActionBarColor(this));
|
this.avatar.setBackgroundColor(recipient.getColor().toActionBarColor(this));
|
||||||
this.toolbarLayout.setTitle(recipient.toShortString(this));
|
this.toolbarLayout.setTitle(recipient.toShortString(this));
|
||||||
this.toolbarLayout.setContentScrimColor(recipient.getColor().toActionBarColor(this));
|
this.toolbarLayout.setContentScrimColor(recipient.getColor().toActionBarColor(this));
|
||||||
if (recipient.getUuid().isPresent()) {
|
|
||||||
toolbarLayout.setOnLongClickListener(v -> {
|
|
||||||
Util.copyToClipboard(this, recipient.getUuid().get().toString());
|
|
||||||
ServiceUtil.getVibrator(this).vibrate(200);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -284,8 +281,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
|
|
||||||
initializeRecipients();
|
initializeRecipients();
|
||||||
|
|
||||||
this.canHaveSafetyNumber = getActivity().getIntent()
|
this.canHaveSafetyNumber = recipient.get().isRegistered() && !recipient.get().isLocalNumber();
|
||||||
.getBooleanExtra(RecipientPreferenceActivity.CAN_HAVE_SAFETY_NUMBER_EXTRA, false);
|
|
||||||
|
|
||||||
Preference customNotificationsPref = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
Preference customNotificationsPref = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||||
|
|
||||||
@@ -451,7 +447,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
aboutPreference.setSummary(recipient.getCustomLabel());
|
aboutPreference.setSummary(recipient.getCustomLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutPreference.setSecure(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED);
|
aboutPreference.setState(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED, recipient.isBlocked());
|
||||||
|
|
||||||
IdentityUtil.getRemoteIdentityKey(getActivity(), recipient).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
|
IdentityUtil.getRemoteIdentityKey(getActivity(), recipient).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -693,11 +689,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
Intent verifyIdentityIntent = new Intent(preference.getContext(), VerifyIdentityActivity.class);
|
startActivity(VerifyIdentityActivity.newIntent(preference.getContext(), identityKey));
|
||||||
verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_EXTRA, recipient.getId());
|
|
||||||
verifyIdentityIntent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey.getIdentityKey()));
|
|
||||||
verifyIdentityIntent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, identityKey.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
|
|
||||||
startActivity(verifyIdentityIntent);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -706,72 +698,15 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
|
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
if (recipient.get().isBlocked()) handleUnblock(preference.getContext());
|
Context context = preference.getContext();
|
||||||
else handleBlock(preference.getContext());
|
|
||||||
|
|
||||||
return true;
|
if (recipient.get().isBlocked()) {
|
||||||
}
|
BlockUnblockDialog.showUnblockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.unblock(context, recipient.get()));
|
||||||
|
} else {
|
||||||
private void handleBlock(@NonNull final Context context) {
|
BlockUnblockDialog.showBlockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.block(context, recipient.get()));
|
||||||
new AsyncTask<Void, Void, Pair<Integer, Integer>>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Pair<Integer, Integer> doInBackground(Void... voids) {
|
|
||||||
int titleRes = R.string.RecipientPreferenceActivity_block_this_contact_question;
|
|
||||||
int bodyRes = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact;
|
|
||||||
|
|
||||||
if (recipient.get().isGroup()) {
|
|
||||||
bodyRes = R.string.RecipientPreferenceActivity_block_and_leave_group_description;
|
|
||||||
|
|
||||||
if (recipient.get().isGroup() && DatabaseFactory.getGroupDatabase(context).isActive(recipient.get().requireGroupId())) {
|
|
||||||
titleRes = R.string.RecipientPreferenceActivity_block_and_leave_group;
|
|
||||||
} else {
|
|
||||||
titleRes = R.string.RecipientPreferenceActivity_block_group;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Pair<>(titleRes, bodyRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Pair<Integer, Integer> titleAndBody) {
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setTitle(titleAndBody.first)
|
|
||||||
.setMessage(titleAndBody.second)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block, (dialog, which) -> {
|
|
||||||
setBlocked(context, recipient.get(), true);
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
}.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleUnblock(@NonNull Context context) {
|
|
||||||
int titleRes = R.string.RecipientPreferenceActivity_unblock_this_contact_question;
|
|
||||||
int bodyRes = R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact;
|
|
||||||
|
|
||||||
if (recipient.resolve().isGroup()) {
|
|
||||||
titleRes = R.string.RecipientPreferenceActivity_unblock_this_group_question;
|
|
||||||
bodyRes = R.string.RecipientPreferenceActivity_unblock_this_group_description;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
return true;
|
||||||
.setTitle(titleRes)
|
|
||||||
.setMessage(bodyRes)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, (dialog, which) -> setBlocked(context, recipient.get(), false)).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setBlocked(@NonNull final Context context, final Recipient recipient, final boolean blocked) {
|
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
|
||||||
if (blocked) {
|
|
||||||
RecipientUtil.block(context, recipient);
|
|
||||||
} else {
|
|
||||||
RecipientUtil.unblock(context, recipient);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,15 +729,15 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInSecureCallClicked() {
|
public void onInSecureCallClicked() {
|
||||||
try {
|
CommunicationActions.startInsecureCall(requireActivity(), recipient.get());
|
||||||
Intent dialIntent = new Intent(Intent.ACTION_DIAL,
|
}
|
||||||
Uri.parse("tel:" + recipient.get().requireE164()));
|
|
||||||
startActivity(dialIntent);
|
@Override
|
||||||
} catch (ActivityNotFoundException anfe) {
|
public void onLongClick() {
|
||||||
Log.w(TAG, anfe);
|
if (recipient.get().hasE164()) {
|
||||||
Dialogs.showAlertDialog(getContext(),
|
Util.copyToClipboard(requireContext(), recipient.get().requireE164());
|
||||||
getString(R.string.ConversationActivity_calls_not_supported),
|
ServiceUtil.getVibrator(requireContext()).vibrate(250);
|
||||||
getString(R.string.ConversationActivity_this_device_does_not_appear_to_support_dial_actions));
|
Toast.makeText(requireContext(), R.string.RecipientBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public class TransportOptions {
|
|||||||
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
|
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
|
||||||
return new TransportOption(Type.TEXTSECURE,
|
return new TransportOption(Type.TEXTSECURE,
|
||||||
R.drawable.ic_send_lock_24,
|
R.drawable.ic_send_lock_24,
|
||||||
context.getResources().getColor(R.color.textsecure_primary),
|
context.getResources().getColor(R.color.core_ultramarine),
|
||||||
context.getString(R.string.ConversationActivity_transport_signal),
|
context.getString(R.string.ConversationActivity_transport_signal),
|
||||||
context.getString(R.string.conversation_activity__type_message_push),
|
context.getString(R.string.conversation_activity__type_message_push),
|
||||||
new PushCharacterCalculator());
|
new PushCharacterCalculator());
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
|
|
||||||
public class TypingIndicatorIntroFragment extends Fragment {
|
|
||||||
|
|
||||||
private Controller controller;
|
|
||||||
|
|
||||||
public static TypingIndicatorIntroFragment newInstance() {
|
|
||||||
TypingIndicatorIntroFragment fragment = new TypingIndicatorIntroFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TypingIndicatorIntroFragment() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
|
|
||||||
if (!(getActivity() instanceof Controller)) {
|
|
||||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
|
||||||
}
|
|
||||||
|
|
||||||
controller = (Controller) getActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.experience_upgrade_typing_indicators_fragment, container, false);
|
|
||||||
View yesButton = view.findViewById(R.id.experience_yes_button);
|
|
||||||
View noButton = view.findViewById(R.id.experience_no_button);
|
|
||||||
|
|
||||||
((TypingIndicatorView) view.findViewById(R.id.typing_indicator)).startAnimation();
|
|
||||||
|
|
||||||
yesButton.setOnClickListener(v -> onButtonClicked(true));
|
|
||||||
noButton.setOnClickListener(v -> onButtonClicked(false));
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onButtonClicked(boolean typingEnabled) {
|
|
||||||
TextSecurePreferences.setTypingIndicatorsEnabled(getContext(), typingEnabled);
|
|
||||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
|
||||||
typingEnabled,
|
|
||||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
|
||||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
|
||||||
|
|
||||||
controller.onTypingIndicatorsFinished();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Controller {
|
|
||||||
void onTypingIndicatorsFinished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,17 +34,9 @@ import android.os.AsyncTask;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Vibrator;
|
import android.os.Vibrator;
|
||||||
import androidx.annotation.DrawableRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
|
||||||
import androidx.appcompat.widget.SwitchCompat;
|
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -62,13 +54,22 @@ import android.widget.ImageView;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.SwitchCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.qr.QrCode;
|
import org.thoughtcrime.securesms.qr.QrCode;
|
||||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||||
@@ -76,8 +77,8 @@ import org.thoughtcrime.securesms.qr.ScanningThread;
|
|||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
|
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||||
@@ -105,22 +106,53 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
|||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements ScanListener, View.OnClickListener {
|
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements ScanListener, View.OnClickListener {
|
||||||
|
|
||||||
private static final String TAG = VerifyIdentityActivity.class.getSimpleName();
|
private static final String TAG = Log.tag(VerifyIdentityActivity.class);
|
||||||
|
|
||||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
private static final String RECIPIENT_EXTRA = "recipient_id";
|
||||||
public static final String IDENTITY_EXTRA = "recipient_identity";
|
private static final String IDENTITY_EXTRA = "recipient_identity";
|
||||||
public static final String VERIFIED_EXTRA = "verified_state";
|
private static final String VERIFIED_EXTRA = "verified_state";
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
|
private final DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
|
||||||
|
|
||||||
private VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
|
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
|
||||||
private VerifyScanFragment scanFragment = new VerifyScanFragment();
|
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
|
||||||
|
|
||||||
|
public static Intent newIntent(@NonNull Context context,
|
||||||
|
@NonNull IdentityDatabase.IdentityRecord identityRecord)
|
||||||
|
{
|
||||||
|
return newIntent(context,
|
||||||
|
identityRecord.getRecipientId(),
|
||||||
|
identityRecord.getIdentityKey(),
|
||||||
|
identityRecord.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Intent newIntent(@NonNull Context context,
|
||||||
|
@NonNull IdentityDatabase.IdentityRecord identityRecord,
|
||||||
|
boolean verified)
|
||||||
|
{
|
||||||
|
return newIntent(context,
|
||||||
|
identityRecord.getRecipientId(),
|
||||||
|
identityRecord.getIdentityKey(),
|
||||||
|
verified);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Intent newIntent(@NonNull Context context,
|
||||||
|
@NonNull RecipientId recipientId,
|
||||||
|
@NonNull IdentityKey identityKey,
|
||||||
|
boolean verified)
|
||||||
|
{
|
||||||
|
Intent intent = new Intent(context, VerifyIdentityActivity.class);
|
||||||
|
|
||||||
|
intent.putExtra(RECIPIENT_EXTRA, recipientId);
|
||||||
|
intent.putExtra(IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey));
|
||||||
|
intent.putExtra(VERIFIED_EXTRA, verified);
|
||||||
|
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreCreate() {
|
public void onPreCreate() {
|
||||||
dynamicTheme.onCreate(this);
|
dynamicTheme.onCreate(this);
|
||||||
dynamicLanguage.onCreate(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -143,7 +175,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
|||||||
scanFragment.setScanListener(this);
|
scanFragment.setScanListener(this);
|
||||||
displayFragment.setClickListener(this);
|
displayFragment.setClickListener(this);
|
||||||
|
|
||||||
initFragment(android.R.id.content, displayFragment, dynamicLanguage.getCurrentLocale(), extras);
|
initFragment(android.R.id.content, displayFragment, Locale.getDefault(), extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -604,6 +636,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
|||||||
remoteIdentity,
|
remoteIdentity,
|
||||||
isChecked ? VerifiedStatus.VERIFIED :
|
isChecked ? VerifiedStatus.VERIFIED :
|
||||||
VerifiedStatus.DEFAULT));
|
VerifiedStatus.DEFAULT));
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange();
|
||||||
|
|
||||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), isChecked, false);
|
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), isChecked, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,43 +18,56 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.Activity;
|
import android.app.PictureInPictureParams;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.DialogInterface.OnClickListener;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
import android.text.SpannableString;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import android.text.Spanned;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import android.util.Rational;
|
||||||
import android.view.View;
|
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
import org.greenrobot.eventbus.ThreadMode;
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAnswerDeclineButton;
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
|
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
|
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||||
|
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.webrtc.SurfaceViewRenderer;
|
|
||||||
import org.whispersystems.libsignal.IdentityKey;
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||||
|
|
||||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||||
|
|
||||||
public class WebRtcCallActivity extends Activity {
|
public class WebRtcCallActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
|
||||||
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
|
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
|
||||||
|
|
||||||
@@ -67,8 +80,10 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
|
|
||||||
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
|
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
|
||||||
|
|
||||||
private WebRtcCallScreen callScreen;
|
private WebRtcCallView callScreen;
|
||||||
private boolean enableVideoIfAvailable;
|
private TooltipPopup videoTooltip;
|
||||||
|
private WebRtcCallViewModel viewModel;
|
||||||
|
private boolean enableVideoIfAvailable;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -79,10 +94,12 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
|
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
setContentView(R.layout.webrtc_call_activity);
|
setContentView(R.layout.webrtc_call_activity);
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
|
||||||
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||||
|
|
||||||
initializeResources();
|
initializeResources();
|
||||||
|
initializeViewModel();
|
||||||
|
|
||||||
processIntent(getIntent());
|
processIntent(getIntent());
|
||||||
|
|
||||||
@@ -90,18 +107,21 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
|
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
Log.i(TAG, "onResume()");
|
Log.i(TAG, "onResume()");
|
||||||
super.onResume();
|
super.onResume();
|
||||||
initializeScreenshotSecurity();
|
initializeScreenshotSecurity();
|
||||||
EventBus.getDefault().register(this);
|
|
||||||
|
if (!EventBus.getDefault().isRegistered(this)) {
|
||||||
|
EventBus.getDefault().register(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewIntent(Intent intent){
|
public void onNewIntent(Intent intent){
|
||||||
Log.i(TAG, "onNewIntent");
|
Log.i(TAG, "onNewIntent");
|
||||||
|
super.onNewIntent(intent);
|
||||||
processIntent(intent);
|
processIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +129,17 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
public void onPause() {
|
public void onPause() {
|
||||||
Log.i(TAG, "onPause");
|
Log.i(TAG, "onPause");
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
|
if (!isInPipMode()) {
|
||||||
|
EventBus.getDefault().unregister(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
Log.i(TAG, "onStop");
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
EventBus.getDefault().unregister(this);
|
EventBus.getDefault().unregister(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +153,32 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onUserLeaveHint() {
|
||||||
|
if (deviceSupportsPipMode()) {
|
||||||
|
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||||
|
.setAspectRatio(new Rational(16, 9))
|
||||||
|
.build();
|
||||||
|
setPictureInPictureParams(params);
|
||||||
|
|
||||||
|
//noinspection deprecation
|
||||||
|
enterPictureInPictureMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
|
||||||
|
viewModel.setIsInPipMode(isInPictureInPictureMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInPipMode() {
|
||||||
|
return deviceSupportsPipMode() && isInPictureInPictureMode();
|
||||||
|
}
|
||||||
|
|
||||||
private void processIntent(@NonNull Intent intent) {
|
private void processIntent(@NonNull Intent intent) {
|
||||||
if (ANSWER_ACTION.equals(intent.getAction())) {
|
if (ANSWER_ACTION.equals(intent.getAction())) {
|
||||||
handleAnswerCall();
|
viewModel.setRecipient(EventBus.getDefault().getStickyEvent(WebRtcViewModel.class).getRecipient());
|
||||||
|
handleAnswerWithAudio();
|
||||||
} else if (DENY_ACTION.equals(intent.getAction())) {
|
} else if (DENY_ACTION.equals(intent.getAction())) {
|
||||||
handleDenyCall();
|
handleDenyCall();
|
||||||
} else if (END_CALL_ACTION.equals(intent.getAction())) {
|
} else if (END_CALL_ACTION.equals(intent.getAction())) {
|
||||||
@@ -142,26 +196,77 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
|
|
||||||
private void initializeResources() {
|
private void initializeResources() {
|
||||||
callScreen = ViewUtil.findById(this, R.id.callScreen);
|
callScreen = ViewUtil.findById(this, R.id.callScreen);
|
||||||
callScreen.setHangupButtonListener(new HangupButtonListener());
|
callScreen.setControlsListener(new ControlsListener());
|
||||||
callScreen.setIncomingCallActionListener(new IncomingCallActionListener());
|
|
||||||
callScreen.setAudioMuteButtonListener(new AudioMuteButtonListener());
|
|
||||||
callScreen.setVideoMuteButtonListener(new VideoMuteButtonListener());
|
|
||||||
callScreen.setCameraFlipButtonListener(new CameraFlipButtonListener());
|
|
||||||
callScreen.setSpeakerButtonListener(new SpeakerButtonListener());
|
|
||||||
callScreen.setBluetoothButtonListener(new BluetoothButtonListener());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSetAudioSpeaker(boolean enabled) {
|
private void initializeViewModel() {
|
||||||
|
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
|
||||||
|
viewModel.setIsInPipMode(isInPipMode());
|
||||||
|
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
|
||||||
|
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||||
|
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
|
||||||
|
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
|
||||||
|
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||||
|
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||||
|
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||||
|
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
||||||
|
if (isInPipMode()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case SHOW_VIDEO_TOOLTIP:
|
||||||
|
if (videoTooltip == null) {
|
||||||
|
videoTooltip = TooltipPopup.forTarget(callScreen.getVideoTooltipTarget())
|
||||||
|
.setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
|
||||||
|
.setTextColor(ContextCompat.getColor(this, R.color.core_white))
|
||||||
|
.setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video)
|
||||||
|
.setOnDismissListener(() -> viewModel.onDismissedVideoTooltip())
|
||||||
|
.show(TooltipPopup.POSITION_ABOVE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DISMISS_VIDEO_TOOLTIP:
|
||||||
|
if (videoTooltip != null) {
|
||||||
|
videoTooltip.dismiss();
|
||||||
|
videoTooltip = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown event: " + event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCallTime(long callTime) {
|
||||||
|
EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(callTime);
|
||||||
|
|
||||||
|
if (ellapsedTimeFormatter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSetAudioHandset() {
|
||||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||||
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_SPEAKER);
|
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_SPEAKER);
|
||||||
intent.putExtra(WebRtcCallService.EXTRA_SPEAKER, enabled);
|
|
||||||
startService(intent);
|
startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSetAudioBluetooth(boolean enabled) {
|
private void handleSetAudioSpeaker() {
|
||||||
|
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||||
|
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_SPEAKER);
|
||||||
|
intent.putExtra(WebRtcCallService.EXTRA_SPEAKER, true);
|
||||||
|
startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSetAudioBluetooth() {
|
||||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||||
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_BLUETOOTH);
|
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_BLUETOOTH);
|
||||||
intent.putExtra(WebRtcCallService.EXTRA_BLUETOOTH, enabled);
|
intent.putExtra(WebRtcCallService.EXTRA_BLUETOOTH, true);
|
||||||
startService(intent);
|
startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,10 +278,24 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleSetMuteVideo(boolean muted) {
|
private void handleSetMuteVideo(boolean muted) {
|
||||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
Recipient recipient = viewModel.getRecipient().get();
|
||||||
intent.setAction(WebRtcCallService.ACTION_SET_ENABLE_VIDEO);
|
|
||||||
intent.putExtra(WebRtcCallService.EXTRA_ENABLE, !muted);
|
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||||
startService(intent);
|
String recipientDisplayName = recipient.getDisplayName(this);
|
||||||
|
|
||||||
|
Permissions.with(this)
|
||||||
|
.request(Manifest.permission.CAMERA)
|
||||||
|
.ifNecessary()
|
||||||
|
.withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted)
|
||||||
|
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName))
|
||||||
|
.onAllGranted(() -> {
|
||||||
|
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||||
|
intent.setAction(WebRtcCallService.ACTION_SET_ENABLE_VIDEO);
|
||||||
|
intent.putExtra(WebRtcCallService.EXTRA_ENABLE, !muted);
|
||||||
|
startService(intent);
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleFlipCamera() {
|
private void handleFlipCamera() {
|
||||||
@@ -185,18 +304,19 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
startService(intent);
|
startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAnswerCall() {
|
private void handleAnswerWithAudio() {
|
||||||
WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
|
Recipient recipient = viewModel.getRecipient().get();
|
||||||
|
|
||||||
if (event != null) {
|
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
.request(Manifest.permission.RECORD_AUDIO)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, event.getRecipient().toShortString(this)),
|
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
|
||||||
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
R.drawable.ic_mic_solid_24)
|
||||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering), event.getLocalRenderer());
|
callScreen.setRecipient(recipient);
|
||||||
|
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||||
|
|
||||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||||
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
|
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
|
||||||
@@ -207,15 +327,42 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDenyCall() {
|
private void handleAnswerWithVideo() {
|
||||||
WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
|
Recipient recipient = viewModel.getRecipient().get();
|
||||||
|
|
||||||
if (event != null) {
|
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||||
|
Permissions.with(this)
|
||||||
|
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||||
|
.ifNecessary()
|
||||||
|
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
|
||||||
|
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
||||||
|
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||||
|
.onAllGranted(() -> {
|
||||||
|
callScreen.setRecipient(recipient);
|
||||||
|
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||||
|
|
||||||
|
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||||
|
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
|
||||||
|
intent.putExtra(WebRtcCallService.EXTRA_ANSWER_WITH_VIDEO, true);
|
||||||
|
startService(intent);
|
||||||
|
|
||||||
|
handleSetMuteVideo(false);
|
||||||
|
})
|
||||||
|
.onAnyDenied(this::handleDenyCall)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDenyCall() {
|
||||||
|
Recipient recipient = viewModel.getRecipient().get();
|
||||||
|
|
||||||
|
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||||
intent.setAction(WebRtcCallService.ACTION_DENY_CALL);
|
intent.setAction(WebRtcCallService.ACTION_DENY_CALL);
|
||||||
startService(intent);
|
startService(intent);
|
||||||
|
|
||||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_ending_call), event.getLocalRenderer());
|
callScreen.setRecipient(recipient);
|
||||||
|
callScreen.setStatus(getString(R.string.RedPhone_ending_call));
|
||||||
delayedFinish();
|
delayedFinish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,46 +375,54 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
|
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
|
||||||
callScreen.setIncomingCall(event.getRecipient());
|
callScreen.setRecipient(event.getRecipient());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
||||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_dialing), event.getLocalRenderer());
|
callScreen.setRecipient(event.getRecipient());
|
||||||
|
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTerminate(@NonNull Recipient recipient, @NonNull SurfaceViewRenderer localRenderer /*, int terminationType */) {
|
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
|
||||||
Log.i(TAG, "handleTerminate called");
|
Log.i(TAG, "handleTerminate called: " + hangupType.name());
|
||||||
|
|
||||||
|
callScreen.setRecipient(recipient);
|
||||||
|
callScreen.setStatusFromHangupType(hangupType);
|
||||||
|
|
||||||
callScreen.setActiveCall(recipient, getString(R.string.RedPhone_ending_call), localRenderer);
|
|
||||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||||
|
|
||||||
delayedFinish();
|
delayedFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCallRinging(@NonNull WebRtcViewModel event) {
|
private void handleCallRinging(@NonNull WebRtcViewModel event) {
|
||||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_ringing), event.getLocalRenderer());
|
callScreen.setRecipient(event.getRecipient());
|
||||||
|
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCallBusy(@NonNull WebRtcViewModel event) {
|
private void handleCallBusy(@NonNull WebRtcViewModel event) {
|
||||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_busy), event.getLocalRenderer());
|
|
||||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||||
|
callScreen.setRecipient(event.getRecipient());
|
||||||
|
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
||||||
|
|
||||||
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
|
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
|
||||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_connected), "", event.getLocalRenderer(), event.getRemoteRenderer());
|
callScreen.setRecipient(event.getRecipient());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
|
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
|
||||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_recipient_unavailable), event.getLocalRenderer());
|
|
||||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||||
|
callScreen.setRecipient(event.getRecipient());
|
||||||
|
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
|
||||||
delayedFinish();
|
delayedFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleServerFailure(@NonNull WebRtcViewModel event) {
|
private void handleServerFailure(@NonNull WebRtcViewModel event) {
|
||||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_network_failed), event.getLocalRenderer());
|
|
||||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||||
|
callScreen.setRecipient(event.getRecipient());
|
||||||
|
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
|
||||||
delayedFinish();
|
delayedFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,44 +436,63 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
|
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), event.getLocalRenderer());
|
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCancel(DialogInterface dialog) {
|
public void onCancel(DialogInterface dialog) {
|
||||||
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), event.getLocalRenderer());
|
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
||||||
final IdentityKey theirIdentity = event.getIdentityKey();
|
final IdentityKey theirKey = event.getIdentityKey();
|
||||||
final Recipient recipient = event.getRecipient();
|
final Recipient recipient = event.getRecipient();
|
||||||
|
|
||||||
callScreen.setUntrustedIdentity(recipient, theirIdentity);
|
if (theirKey == null) {
|
||||||
callScreen.setAcceptIdentityListener(new View.OnClickListener() {
|
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||||
@Override
|
}
|
||||||
public void onClick(View v) {
|
|
||||||
synchronized (SESSION_LOCK) {
|
|
||||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
|
|
||||||
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirIdentity, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
String name = recipient.getDisplayName(this);
|
||||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name);
|
||||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
|
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
|
||||||
startService(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
callScreen.setCancelIdentityButton(new View.OnClickListener() {
|
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this);
|
||||||
handleTerminate(recipient, event.getLocalRenderer());
|
untrustedIdentityExplanation.setText(spannableString);
|
||||||
}
|
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
});
|
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setView(untrustedIdentityExplanation)
|
||||||
|
.setPositiveButton(R.string.WebRtcCallScreen_accept, (d, w) -> {
|
||||||
|
synchronized (SESSION_LOCK) {
|
||||||
|
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
|
||||||
|
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
d.dismiss();
|
||||||
|
|
||||||
|
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||||
|
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||||
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
|
||||||
|
|
||||||
|
startService(intent);
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.WebRtcCallScreen_end_call, (d, w) -> {
|
||||||
|
d.dismiss();
|
||||||
|
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean deviceSupportsPipMode() {
|
||||||
|
return Build.VERSION.SDK_INT >= 26 &&
|
||||||
|
FeatureFlags.callingPip() &&
|
||||||
|
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void delayedFinish() {
|
private void delayedFinish() {
|
||||||
@@ -326,34 +500,35 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void delayedFinish(int delayMillis) {
|
private void delayedFinish(int delayMillis) {
|
||||||
callScreen.postDelayed(new Runnable() {
|
callScreen.postDelayed(WebRtcCallActivity.this::finish, delayMillis);
|
||||||
public void run() {
|
|
||||||
WebRtcCallActivity.this.finish();
|
|
||||||
}
|
|
||||||
}, delayMillis);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||||
public void onEventMainThread(final WebRtcViewModel event) {
|
public void onEventMainThread(final WebRtcViewModel event) {
|
||||||
Log.i(TAG, "Got message from service: " + event);
|
Log.i(TAG, "Got message from service: " + event);
|
||||||
|
|
||||||
|
viewModel.setRecipient(event.getRecipient());
|
||||||
|
|
||||||
switch (event.getState()) {
|
switch (event.getState()) {
|
||||||
case CALL_CONNECTED: handleCallConnected(event); break;
|
case CALL_CONNECTED: handleCallConnected(event); break;
|
||||||
case NETWORK_FAILURE: handleServerFailure(event); break;
|
case NETWORK_FAILURE: handleServerFailure(event); break;
|
||||||
case CALL_RINGING: handleCallRinging(event); break;
|
case CALL_RINGING: handleCallRinging(event); break;
|
||||||
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), event.getLocalRenderer()); break;
|
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
|
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
|
||||||
case CALL_INCOMING: handleIncomingCall(event); break;
|
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||||
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||||
case CALL_BUSY: handleCallBusy(event); break;
|
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
|
||||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
case CALL_INCOMING: handleIncomingCall(event); break;
|
||||||
|
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
||||||
|
case CALL_BUSY: handleCallBusy(event); break;
|
||||||
|
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
callScreen.setRemoteVideoEnabled(event.isRemoteVideoEnabled());
|
callScreen.setLocalRenderer(event.getLocalRenderer());
|
||||||
callScreen.updateAudioState(event.isBluetoothAvailable(), event.isMicrophoneEnabled());
|
callScreen.setRemoteRenderer(event.getRemoteRenderer());
|
||||||
callScreen.setControlsEnabled(event.getState() != WebRtcViewModel.State.CALL_INCOMING);
|
|
||||||
callScreen.setLocalVideoState(event.getLocalCameraState(), event.getLocalRenderer());
|
viewModel.updateFromWebRtcViewModel(event);
|
||||||
|
|
||||||
if (event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable) {
|
if (event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable) {
|
||||||
enableVideoIfAvailable = false;
|
enableVideoIfAvailable = false;
|
||||||
@@ -361,56 +536,74 @@ public class WebRtcCallActivity extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class HangupButtonListener implements WebRtcCallScreen.HangupButtonListener {
|
private final class ControlsListener implements WebRtcCallView.ControlsListener {
|
||||||
public void onClick() {
|
|
||||||
|
@Override
|
||||||
|
public void onControlsFadeOut() {
|
||||||
|
if (videoTooltip != null) {
|
||||||
|
videoTooltip.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
|
||||||
|
switch (audioOutput) {
|
||||||
|
case HANDSET:
|
||||||
|
handleSetAudioHandset();
|
||||||
|
break;
|
||||||
|
case HEADSET:
|
||||||
|
handleSetAudioBluetooth();
|
||||||
|
break;
|
||||||
|
case SPEAKER:
|
||||||
|
handleSetAudioSpeaker();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unknown output: " + audioOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onVideoChanged(boolean isVideoEnabled) {
|
||||||
|
handleSetMuteVideo(!isVideoEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMicChanged(boolean isMicEnabled) {
|
||||||
|
handleSetMuteAudio(!isMicEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCameraDirectionChanged() {
|
||||||
|
handleFlipCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEndCallPressed() {
|
||||||
handleEndCall();
|
handleEndCall();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class AudioMuteButtonListener implements WebRtcCallControls.MuteButtonListener {
|
|
||||||
@Override
|
@Override
|
||||||
public void onToggle(boolean isMuted) {
|
public void onDenyCallPressed() {
|
||||||
WebRtcCallActivity.this.handleSetMuteAudio(isMuted);
|
handleDenyCall();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class VideoMuteButtonListener implements WebRtcCallControls.MuteButtonListener {
|
|
||||||
@Override
|
|
||||||
public void onToggle(boolean isMuted) {
|
|
||||||
WebRtcCallActivity.this.handleSetMuteVideo(isMuted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CameraFlipButtonListener implements WebRtcCallControls.CameraFlipButtonListener {
|
|
||||||
@Override
|
|
||||||
public void onToggle() {
|
|
||||||
WebRtcCallActivity.this.handleFlipCamera();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SpeakerButtonListener implements WebRtcCallControls.SpeakerButtonListener {
|
|
||||||
@Override
|
|
||||||
public void onSpeakerChange(boolean isSpeaker) {
|
|
||||||
WebRtcCallActivity.this.handleSetAudioSpeaker(isSpeaker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BluetoothButtonListener implements WebRtcCallControls.BluetoothButtonListener {
|
|
||||||
@Override
|
|
||||||
public void onBluetoothChange(boolean isBluetooth) {
|
|
||||||
WebRtcCallActivity.this.handleSetAudioBluetooth(isBluetooth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class IncomingCallActionListener implements WebRtcAnswerDeclineButton.AnswerDeclineListener {
|
|
||||||
@Override
|
|
||||||
public void onAnswered() {
|
|
||||||
WebRtcCallActivity.this.handleAnswerCall();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDeclined() {
|
public void onAcceptCallWithVoiceOnlyPressed() {
|
||||||
WebRtcCallActivity.this.handleDenyCall();
|
handleAnswerWithAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAcceptCallPressed() {
|
||||||
|
if (viewModel.isAnswerWithVideoAvailable()) {
|
||||||
|
handleAnswerWithVideo();
|
||||||
|
} else {
|
||||||
|
handleAnswerWithAudio();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownCaretPressed() {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.attachments;
|
package org.thoughtcrime.securesms.attachments;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||||
@@ -19,6 +21,8 @@ public abstract class Attachment {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final String fileName;
|
private final String fileName;
|
||||||
|
|
||||||
|
private final int cdnNumber;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String location;
|
private final String location;
|
||||||
|
|
||||||
@@ -35,10 +39,10 @@ public abstract class Attachment {
|
|||||||
private final String fastPreflightId;
|
private final String fastPreflightId;
|
||||||
|
|
||||||
private final boolean voiceNote;
|
private final boolean voiceNote;
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
private final int height;
|
||||||
|
|
||||||
private final boolean quote;
|
private final boolean quote;
|
||||||
|
private final long uploadTimestamp;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String caption;
|
private final String caption;
|
||||||
@@ -49,19 +53,24 @@ public abstract class Attachment {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final BlurHash blurHash;
|
private final BlurHash blurHash;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final AudioHash audioHash;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final TransformProperties transformProperties;
|
private final TransformProperties transformProperties;
|
||||||
|
|
||||||
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
|
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
|
||||||
@Nullable String location, @Nullable String key, @Nullable String relay,
|
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
|
||||||
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
||||||
int width, int height, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption,
|
||||||
@Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
|
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
|
||||||
|
@Nullable TransformProperties transformProperties)
|
||||||
{
|
{
|
||||||
this.contentType = contentType;
|
this.contentType = contentType;
|
||||||
this.transferState = transferState;
|
this.transferState = transferState;
|
||||||
this.size = size;
|
this.size = size;
|
||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
|
this.cdnNumber = cdnNumber;
|
||||||
this.location = location;
|
this.location = location;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.relay = relay;
|
this.relay = relay;
|
||||||
@@ -71,9 +80,11 @@ public abstract class Attachment {
|
|||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.quote = quote;
|
this.quote = quote;
|
||||||
|
this.uploadTimestamp = uploadTimestamp;
|
||||||
this.stickerLocator = stickerLocator;
|
this.stickerLocator = stickerLocator;
|
||||||
this.caption = caption;
|
this.caption = caption;
|
||||||
this.blurHash = blurHash;
|
this.blurHash = blurHash;
|
||||||
|
this.audioHash = audioHash;
|
||||||
this.transformProperties = transformProperties != null ? transformProperties : TransformProperties.empty();
|
this.transformProperties = transformProperties != null ? transformProperties : TransformProperties.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +117,10 @@ public abstract class Attachment {
|
|||||||
return contentType;
|
return contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCdnNumber() {
|
||||||
|
return cdnNumber;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getLocation() {
|
public String getLocation() {
|
||||||
return location;
|
return location;
|
||||||
@@ -147,6 +162,10 @@ public abstract class Attachment {
|
|||||||
return quote;
|
return quote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getUploadTimestamp() {
|
||||||
|
return uploadTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSticker() {
|
public boolean isSticker() {
|
||||||
return stickerLocator != null;
|
return stickerLocator != null;
|
||||||
}
|
}
|
||||||
@@ -159,6 +178,10 @@ public abstract class Attachment {
|
|||||||
return blurHash;
|
return blurHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @Nullable AudioHash getAudioHash() {
|
||||||
|
return audioHash;
|
||||||
|
}
|
||||||
|
|
||||||
public @Nullable String getCaption() {
|
public @Nullable String getCaption() {
|
||||||
return caption;
|
return caption;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ package org.thoughtcrime.securesms.attachments;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||||
@@ -24,13 +23,14 @@ public class DatabaseAttachment extends Attachment {
|
|||||||
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
|
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
|
||||||
boolean hasData, boolean hasThumbnail,
|
boolean hasData, boolean hasThumbnail,
|
||||||
String contentType, int transferProgress, long size,
|
String contentType, int transferProgress, long size,
|
||||||
String fileName, String location, String key, String relay,
|
String fileName, int cdnNumber, String location, String key, String relay,
|
||||||
byte[] digest, String fastPreflightId, boolean voiceNote,
|
byte[] digest, String fastPreflightId, boolean voiceNote,
|
||||||
int width, int height, boolean quote, @Nullable String caption,
|
int width, int height, boolean quote, @Nullable String caption,
|
||||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
|
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
|
||||||
@Nullable TransformProperties transformProperties, int displayOrder)
|
@Nullable TransformProperties transformProperties, int displayOrder,
|
||||||
|
long uploadTimestamp)
|
||||||
{
|
{
|
||||||
super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, caption, stickerLocator, blurHash, transformProperties);
|
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||||
this.attachmentId = attachmentId;
|
this.attachmentId = attachmentId;
|
||||||
this.hasData = hasData;
|
this.hasData = hasData;
|
||||||
this.hasThumbnail = hasThumbnail;
|
this.hasThumbnail = hasThumbnail;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
|||||||
public class MmsNotificationAttachment extends Attachment {
|
public class MmsNotificationAttachment extends Attachment {
|
||||||
|
|
||||||
public MmsNotificationAttachment(int status, long size) {
|
public MmsNotificationAttachment(int status, long size) {
|
||||||
super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false, null, null, null, null);
|
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.net.Uri;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||||
@@ -18,13 +19,13 @@ import java.util.List;
|
|||||||
public class PointerAttachment extends Attachment {
|
public class PointerAttachment extends Attachment {
|
||||||
|
|
||||||
private PointerAttachment(@NonNull String contentType, int transferState, long size,
|
private PointerAttachment(@NonNull String contentType, int transferState, long size,
|
||||||
@Nullable String fileName, @NonNull String location,
|
@Nullable String fileName, int cdnNumber, @NonNull String location,
|
||||||
@Nullable String key, @Nullable String relay,
|
@Nullable String key, @Nullable String relay,
|
||||||
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
||||||
int width, int height, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||||
@Nullable BlurHash blurHash)
|
@Nullable BlurHash blurHash)
|
||||||
{
|
{
|
||||||
super(contentType, transferState, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, caption, stickerLocator, blurHash, null);
|
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -93,13 +94,15 @@ public class PointerAttachment extends Attachment {
|
|||||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||||
pointer.get().asPointer().getSize().or(0),
|
pointer.get().asPointer().getSize().or(0),
|
||||||
pointer.get().asPointer().getFileName().orNull(),
|
pointer.get().asPointer().getFileName().orNull(),
|
||||||
String.valueOf(pointer.get().asPointer().getId()),
|
pointer.get().asPointer().getCdnNumber(),
|
||||||
|
pointer.get().asPointer().getRemoteId().toString(),
|
||||||
encodedKey, null,
|
encodedKey, null,
|
||||||
pointer.get().asPointer().getDigest().orNull(),
|
pointer.get().asPointer().getDigest().orNull(),
|
||||||
fastPreflightId,
|
fastPreflightId,
|
||||||
pointer.get().asPointer().getVoiceNote(),
|
pointer.get().asPointer().getVoiceNote(),
|
||||||
pointer.get().asPointer().getWidth(),
|
pointer.get().asPointer().getWidth(),
|
||||||
pointer.get().asPointer().getHeight(),
|
pointer.get().asPointer().getHeight(),
|
||||||
|
pointer.get().asPointer().getUploadTimestamp(),
|
||||||
pointer.get().asPointer().getCaption().orNull(),
|
pointer.get().asPointer().getCaption().orNull(),
|
||||||
stickerLocator,
|
stickerLocator,
|
||||||
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
|
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
|
||||||
@@ -113,7 +116,8 @@ public class PointerAttachment extends Attachment {
|
|||||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||||
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
|
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
|
||||||
pointer.getFileName(),
|
pointer.getFileName(),
|
||||||
String.valueOf(thumbnail != null ? thumbnail.asPointer().getId() : 0),
|
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
|
||||||
|
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
|
||||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||||
null,
|
null,
|
||||||
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
|
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
|
||||||
@@ -121,6 +125,7 @@ public class PointerAttachment extends Attachment {
|
|||||||
false,
|
false,
|
||||||
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
|
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
|
||||||
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
|
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
|
||||||
|
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
|
||||||
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null,
|
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null,
|
||||||
null,
|
null,
|
||||||
null));
|
null));
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|||||||
public class TombstoneAttachment extends Attachment {
|
public class TombstoneAttachment extends Attachment {
|
||||||
|
|
||||||
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
||||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, null, null, null, null, null, false, 0, 0, quote, null, null, null, null);
|
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms.attachments;
|
package org.thoughtcrime.securesms.attachments;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||||
|
|
||||||
@@ -16,18 +17,18 @@ public class UriAttachment extends Attachment {
|
|||||||
|
|
||||||
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
|
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
|
||||||
@Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption,
|
@Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption,
|
||||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
|
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
|
||||||
{
|
{
|
||||||
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, transformProperties);
|
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
|
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
|
||||||
@NonNull String contentType, int transferState, long size, int width, int height,
|
@NonNull String contentType, int transferState, long size, int width, int height,
|
||||||
@Nullable String fileName, @Nullable String fastPreflightId,
|
@Nullable String fileName, @Nullable String fastPreflightId,
|
||||||
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||||
@Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
|
@Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
|
||||||
{
|
{
|
||||||
super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, caption, stickerLocator, blurHash, transformProperties);
|
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||||
this.dataUri = dataUri;
|
this.dataUri = dataUri;
|
||||||
this.thumbnailUri = thumbnailUri;
|
this.thumbnailUri = thumbnailUri;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.thoughtcrime.securesms.audio;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||||
|
import org.whispersystems.util.Base64;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An AudioHash is a compact string representation of the wave form and duration for an audio file.
|
||||||
|
*/
|
||||||
|
public final class AudioHash {
|
||||||
|
|
||||||
|
@NonNull private final String hash;
|
||||||
|
@NonNull private final AudioWaveFormData audioWaveForm;
|
||||||
|
|
||||||
|
private AudioHash(@NonNull String hash, @NonNull AudioWaveFormData audioWaveForm) {
|
||||||
|
this.hash = hash;
|
||||||
|
this.audioWaveForm = audioWaveForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioHash(@NonNull AudioWaveFormData audioWaveForm) {
|
||||||
|
this(Base64.encodeBytes(audioWaveForm.toByteArray()), audioWaveForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable AudioHash parseOrNull(@Nullable String hash) {
|
||||||
|
if (hash == null) return null;
|
||||||
|
try {
|
||||||
|
return new AudioHash(hash, AudioWaveFormData.parseFrom(Base64.decode(hash)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull AudioWaveFormData getAudioWaveForm() {
|
||||||
|
return audioWaveForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
AudioHash other = (AudioHash) o;
|
||||||
|
return hash.equals(other.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return hash.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull String getHash() {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
package org.thoughtcrime.securesms.audio;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
import android.media.MediaExtractor;
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.LruCache;
|
||||||
|
|
||||||
|
import androidx.annotation.AnyThread;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.core.util.Consumer;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||||
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
||||||
|
import org.thoughtcrime.securesms.media.MediaInput;
|
||||||
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
public final class AudioWaveForm {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(AudioWaveForm.class);
|
||||||
|
|
||||||
|
private static final int BAR_COUNT = 46;
|
||||||
|
private static final int SAMPLES_PER_BAR = 4;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final AudioSlide slide;
|
||||||
|
|
||||||
|
public AudioWaveForm(@NonNull Context context, @NonNull AudioSlide slide) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
this.slide = slide;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final LruCache<String, AudioFileInfo> WAVE_FORM_CACHE = new LruCache<>(200);
|
||||||
|
private static final Executor AUDIO_DECODER_EXECUTOR = new SerialExecutor(SignalExecutors.BOUNDED);
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
public void getWaveForm(@NonNull Consumer<AudioFileInfo> onSuccess, @NonNull Runnable onFailure) {
|
||||||
|
Uri uri = slide.getUri();
|
||||||
|
Attachment attachment = slide.asAttachment();
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
Log.w(TAG, "No uri");
|
||||||
|
Util.runOnMain(onFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(attachment instanceof DatabaseAttachment)) {
|
||||||
|
Log.i(TAG, "Not yet in database");
|
||||||
|
Util.runOnMain(onFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cacheKey = uri.toString();
|
||||||
|
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
|
Log.i(TAG, "Loaded wave form from cache " + cacheKey);
|
||||||
|
Util.runOnMain(() -> onSuccess.accept(cached));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AUDIO_DECODER_EXECUTOR.execute(() -> {
|
||||||
|
AudioFileInfo cachedInExecutor = WAVE_FORM_CACHE.get(cacheKey);
|
||||||
|
if (cachedInExecutor != null) {
|
||||||
|
Log.i(TAG, "Loaded wave form from cache inside executor" + cacheKey);
|
||||||
|
Util.runOnMain(() -> onSuccess.accept(cachedInExecutor));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioHash audioHash = attachment.getAudioHash();
|
||||||
|
if (audioHash != null) {
|
||||||
|
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
|
||||||
|
if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||||
|
Log.w(TAG, "Wave form from database does not match bar count, regenerating " + cacheKey);
|
||||||
|
} else {
|
||||||
|
WAVE_FORM_CACHE.put(cacheKey, audioFileInfo);
|
||||||
|
Log.i(TAG, "Loaded wave form from DB " + cacheKey);
|
||||||
|
Util.runOnMain(() -> onSuccess.accept(audioFileInfo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||||
|
|
||||||
|
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||||
|
|
||||||
|
DatabaseFactory.getAttachmentDatabase(context).writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||||
|
|
||||||
|
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||||
|
Util.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||||
|
Util.runOnMain(onFailure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on decode sample from:
|
||||||
|
* <p>
|
||||||
|
* https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/DecoderTest.java
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
@RequiresApi(api = 23)
|
||||||
|
private @NonNull AudioFileInfo generateWaveForm(@NonNull Uri uri) throws IOException {
|
||||||
|
try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
|
||||||
|
long[] wave = new long[BAR_COUNT];
|
||||||
|
int[] waveSamples = new int[BAR_COUNT];
|
||||||
|
|
||||||
|
MediaExtractor extractor = dataSource.createExtractor();
|
||||||
|
|
||||||
|
if (extractor.getTrackCount() == 0) {
|
||||||
|
throw new IOException("No audio track");
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaFormat format = extractor.getTrackFormat(0);
|
||||||
|
|
||||||
|
if (!format.containsKey(MediaFormat.KEY_DURATION)) {
|
||||||
|
throw new IOException("Unknown duration");
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION);
|
||||||
|
String mime = format.getString(MediaFormat.KEY_MIME);
|
||||||
|
|
||||||
|
if (!mime.startsWith("audio/")) {
|
||||||
|
throw new IOException("Mime not audio");
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaCodec codec = MediaCodec.createDecoderByType(mime);
|
||||||
|
|
||||||
|
if (totalDurationUs == 0) {
|
||||||
|
throw new IOException("Zero duration");
|
||||||
|
}
|
||||||
|
|
||||||
|
codec.configure(format, null, null, 0);
|
||||||
|
codec.start();
|
||||||
|
|
||||||
|
ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
|
||||||
|
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
|
||||||
|
|
||||||
|
extractor.selectTrack(0);
|
||||||
|
|
||||||
|
long kTimeOutUs = 5000;
|
||||||
|
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
|
||||||
|
boolean sawInputEOS = false;
|
||||||
|
boolean sawOutputEOS = false;
|
||||||
|
int noOutputCounter = 0;
|
||||||
|
|
||||||
|
while (!sawOutputEOS && noOutputCounter < 50) {
|
||||||
|
noOutputCounter++;
|
||||||
|
if (!sawInputEOS) {
|
||||||
|
int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
|
||||||
|
if (inputBufIndex >= 0) {
|
||||||
|
ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
|
||||||
|
int sampleSize = extractor.readSampleData(dstBuf, 0);
|
||||||
|
long presentationTimeUs = 0;
|
||||||
|
|
||||||
|
if (sampleSize < 0) {
|
||||||
|
sawInputEOS = true;
|
||||||
|
sampleSize = 0;
|
||||||
|
} else {
|
||||||
|
presentationTimeUs = extractor.getSampleTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
codec.queueInputBuffer(
|
||||||
|
inputBufIndex,
|
||||||
|
0,
|
||||||
|
sampleSize,
|
||||||
|
presentationTimeUs,
|
||||||
|
sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
|
||||||
|
|
||||||
|
if (!sawInputEOS) {
|
||||||
|
int barSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
|
||||||
|
sawInputEOS = !extractor.advance();
|
||||||
|
int nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
|
||||||
|
while (!sawInputEOS && nextBarSampleIndex == barSampleIndex) {
|
||||||
|
sawInputEOS = !extractor.advance();
|
||||||
|
if (!sawInputEOS) {
|
||||||
|
nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int outputBufferIndex;
|
||||||
|
do {
|
||||||
|
outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
|
||||||
|
if (outputBufferIndex >= 0) {
|
||||||
|
if (info.size > 0) {
|
||||||
|
noOutputCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buf = codecOutputBuffers[outputBufferIndex];
|
||||||
|
int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs);
|
||||||
|
long total = 0;
|
||||||
|
for (int i = 0; i < info.size; i += 2 * 4) {
|
||||||
|
short aShort = buf.getShort(i);
|
||||||
|
total += Math.abs(aShort);
|
||||||
|
}
|
||||||
|
if (barIndex >= 0 && barIndex < wave.length) {
|
||||||
|
wave[barIndex] += total;
|
||||||
|
waveSamples[barIndex] += info.size / 2;
|
||||||
|
}
|
||||||
|
codec.releaseOutputBuffer(outputBufferIndex, false);
|
||||||
|
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||||
|
sawOutputEOS = true;
|
||||||
|
}
|
||||||
|
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||||
|
codecOutputBuffers = codec.getOutputBuffers();
|
||||||
|
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||||
|
Log.d(TAG, "output format has changed to " + codec.getOutputFormat());
|
||||||
|
}
|
||||||
|
} while (outputBufferIndex >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
codec.stop();
|
||||||
|
codec.release();
|
||||||
|
extractor.release();
|
||||||
|
|
||||||
|
float[] floats = new float[BAR_COUNT];
|
||||||
|
byte[] bytes = new byte[BAR_COUNT];
|
||||||
|
float max = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < BAR_COUNT; i++) {
|
||||||
|
if (waveSamples[i] == 0) continue;
|
||||||
|
|
||||||
|
floats[i] = wave[i] / (float) waveSamples[i];
|
||||||
|
if (floats[i] > max) {
|
||||||
|
max = floats[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < BAR_COUNT; i++) {
|
||||||
|
float normalized = floats[i] / max;
|
||||||
|
bytes[i] = (byte) (255 * normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AudioFileInfo(totalDurationUs, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AudioFileInfo {
|
||||||
|
private final long durationUs;
|
||||||
|
private final byte[] waveFormBytes;
|
||||||
|
private final float[] waveForm;
|
||||||
|
|
||||||
|
private static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
|
||||||
|
return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AudioFileInfo(long durationUs, byte[] waveFormBytes) {
|
||||||
|
this.durationUs = durationUs;
|
||||||
|
this.waveFormBytes = waveFormBytes;
|
||||||
|
this.waveForm = new float[waveFormBytes.length];
|
||||||
|
|
||||||
|
for (int i = 0; i < waveFormBytes.length; i++) {
|
||||||
|
int unsigned = waveFormBytes[i] & 0xff;
|
||||||
|
this.waveForm[i] = unsigned / 255f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDuration(@NonNull TimeUnit timeUnit) {
|
||||||
|
return timeUnit.convert(durationUs, TimeUnit.MICROSECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[] getWaveForm() {
|
||||||
|
return waveForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull AudioWaveFormData toDatabaseProtobuf() {
|
||||||
|
return AudioWaveFormData.newBuilder()
|
||||||
|
.setDurationUs(durationUs)
|
||||||
|
.setWaveForm(ByteString.copyFrom(waveFormBytes))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.avatar;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
import com.theartofdev.edmodo.cropper.CropImage;
|
|
||||||
import com.theartofdev.edmodo.cropper.CropImageView;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
||||||
import org.thoughtcrime.securesms.util.FileProviderUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static android.provider.MediaStore.EXTRA_OUTPUT;
|
|
||||||
|
|
||||||
public final class AvatarSelection {
|
|
||||||
|
|
||||||
private static final String TAG = AvatarSelection.class.getSimpleName();
|
|
||||||
|
|
||||||
private AvatarSelection() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final int REQUEST_CODE_CROP_IMAGE = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE;
|
|
||||||
public static final int REQUEST_CODE_AVATAR = REQUEST_CODE_CROP_IMAGE + 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns result on {@link #REQUEST_CODE_CROP_IMAGE}
|
|
||||||
*/
|
|
||||||
public static void circularCropImage(Activity activity, Uri inputFile, Uri outputFile, @StringRes int title) {
|
|
||||||
CropImage.activity(inputFile)
|
|
||||||
.setGuidelines(CropImageView.Guidelines.ON)
|
|
||||||
.setAspectRatio(1, 1)
|
|
||||||
.setCropShape(CropImageView.CropShape.OVAL)
|
|
||||||
.setOutputUri(outputFile)
|
|
||||||
.setAllowRotation(true)
|
|
||||||
.setAllowFlipping(true)
|
|
||||||
.setBackgroundColor(ContextCompat.getColor(activity, R.color.avatar_background))
|
|
||||||
.setActivityTitle(activity.getString(title))
|
|
||||||
.start(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns result on {@link #REQUEST_CODE_CROP_IMAGE}
|
|
||||||
*/
|
|
||||||
public static void circularCropImage(Fragment fragment, Uri inputFile, Uri outputFile, @StringRes int title) {
|
|
||||||
CropImage.activity(inputFile)
|
|
||||||
.setGuidelines(CropImageView.Guidelines.ON)
|
|
||||||
.setAspectRatio(1, 1)
|
|
||||||
.setCropShape(CropImageView.CropShape.OVAL)
|
|
||||||
.setOutputUri(outputFile)
|
|
||||||
.setAllowRotation(true)
|
|
||||||
.setAllowFlipping(true)
|
|
||||||
.setBackgroundColor(ContextCompat.getColor(fragment.requireContext(), R.color.avatar_background))
|
|
||||||
.setActivityTitle(fragment.requireContext().getString(title))
|
|
||||||
.start(fragment.requireContext(), fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Uri getResultUri(Intent data) {
|
|
||||||
return CropImage.getActivityResult(data).getUri();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns result on {@link #REQUEST_CODE_AVATAR}
|
|
||||||
*
|
|
||||||
* @return Temporary capture file if created.
|
|
||||||
*/
|
|
||||||
public static File startAvatarSelection(Activity activity, boolean includeClear, boolean attemptToIncludeCamera) {
|
|
||||||
File captureFile = attemptToIncludeCamera ? getCaptureFile(activity) : null;
|
|
||||||
|
|
||||||
Intent chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear);
|
|
||||||
activity.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
|
|
||||||
return captureFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns result on {@link #REQUEST_CODE_AVATAR}
|
|
||||||
*
|
|
||||||
* @return Temporary capture file if created.
|
|
||||||
*/
|
|
||||||
public static File startAvatarSelection(Fragment fragment, boolean includeClear, boolean attemptToIncludeCamera) {
|
|
||||||
File captureFile = attemptToIncludeCamera ? getCaptureFile(fragment.requireContext()) : null;
|
|
||||||
|
|
||||||
Intent chooserIntent = createAvatarSelectionIntent(fragment.requireContext(), captureFile, includeClear);
|
|
||||||
fragment.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
|
|
||||||
return captureFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @Nullable File getCaptureFile(@NonNull Context context) {
|
|
||||||
if (!Permissions.hasAll(context, Manifest.permission.CAMERA)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return File.createTempFile("capture", "jpg", context.getExternalCacheDir());
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Intent createAvatarSelectionIntent(Context context, @Nullable File tempCaptureFile, boolean includeClear) {
|
|
||||||
List<Intent> extraIntents = new LinkedList<>();
|
|
||||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
|
||||||
|
|
||||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
|
||||||
|
|
||||||
if (!IntentUtils.isResolvable(context, galleryIntent)) {
|
|
||||||
galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
||||||
galleryIntent.setType("image/*");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tempCaptureFile != null) {
|
|
||||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
|
||||||
|
|
||||||
if (cameraIntent.resolveActivity(context.getPackageManager()) != null) {
|
|
||||||
cameraIntent.putExtra(EXTRA_OUTPUT, FileProviderUtil.getUriFor(context, tempCaptureFile));
|
|
||||||
extraIntents.add(cameraIntent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeClear) {
|
|
||||||
extraIntents.add(new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent chooserIntent = Intent.createChooser(galleryIntent, context.getString(R.string.CreateProfileActivity_profile_photo));
|
|
||||||
|
|
||||||
if (!extraIntents.isEmpty()) {
|
|
||||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return chooserIntent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,9 +21,9 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
|||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||||
@@ -48,7 +48,6 @@ import java.io.OutputStream;
|
|||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -74,7 +73,8 @@ public class FullBackupExporter extends FullBackupBase {
|
|||||||
SearchDatabase.MMS_FTS_TABLE_NAME,
|
SearchDatabase.MMS_FTS_TABLE_NAME,
|
||||||
JobDatabase.JOBS_TABLE_NAME,
|
JobDatabase.JOBS_TABLE_NAME,
|
||||||
JobDatabase.CONSTRAINTS_TABLE_NAME,
|
JobDatabase.CONSTRAINTS_TABLE_NAME,
|
||||||
JobDatabase.DEPENDENCIES_TABLE_NAME
|
JobDatabase.DEPENDENCIES_TABLE_NAME,
|
||||||
|
KeyValueDatabase.TABLE_NAME
|
||||||
);
|
);
|
||||||
|
|
||||||
public static void export(@NonNull Context context,
|
public static void export(@NonNull Context context,
|
||||||
@@ -116,9 +116,11 @@ public class FullBackupExporter extends FullBackupBase {
|
|||||||
|
|
||||||
stopwatch.split("prefs");
|
stopwatch.split("prefs");
|
||||||
|
|
||||||
for (File avatar : AvatarHelper.getAvatarFiles(context)) {
|
for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) {
|
||||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
if (avatar != null) {
|
||||||
outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length());
|
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
||||||
|
outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopwatch.split("avatars");
|
stopwatch.split("avatars");
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ public class FullBackupImporter extends FullBackupBase {
|
|||||||
|
|
||||||
ContentValues contentValues = new ContentValues();
|
ContentValues contentValues = new ContentValues();
|
||||||
contentValues.put(StickerDatabase.FILE_PATH, dataFile.getAbsolutePath());
|
contentValues.put(StickerDatabase.FILE_PATH, dataFile.getAbsolutePath());
|
||||||
|
contentValues.put(StickerDatabase.FILE_LENGTH, sticker.getLength());
|
||||||
contentValues.put(StickerDatabase.FILE_RANDOM, output.first);
|
contentValues.put(StickerDatabase.FILE_RANDOM, output.first);
|
||||||
|
|
||||||
db.update(StickerDatabase.TABLE_NAME, contentValues,
|
db.update(StickerDatabase.TABLE_NAME, contentValues,
|
||||||
@@ -175,7 +176,7 @@ public class FullBackupImporter extends FullBackupBase {
|
|||||||
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
|
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
|
||||||
if (avatar.hasRecipientId()) {
|
if (avatar.hasRecipientId()) {
|
||||||
RecipientId recipientId = RecipientId.from(avatar.getRecipientId());
|
RecipientId recipientId = RecipientId.from(avatar.getRecipientId());
|
||||||
inputStream.readAttachmentTo(new FileOutputStream(AvatarHelper.getAvatarFile(context, recipientId)), avatar.getLength());
|
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId), avatar.getLength());
|
||||||
} else {
|
} else {
|
||||||
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) {
|
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) {
|
||||||
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");
|
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.color;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.ColorRes;
|
import androidx.annotation.ColorRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -26,6 +27,7 @@ public enum MaterialColor {
|
|||||||
PLUM (R.color.conversation_plumb, R.color.conversation_plumb_tint, R.color.conversation_plumb_shade, "pink"),
|
PLUM (R.color.conversation_plumb, R.color.conversation_plumb_tint, R.color.conversation_plumb_shade, "pink"),
|
||||||
TAUPE (R.color.conversation_taupe, R.color.conversation_taupe_tint, R.color.conversation_taupe_shade, "blue_grey"),
|
TAUPE (R.color.conversation_taupe, R.color.conversation_taupe_tint, R.color.conversation_taupe_shade, "blue_grey"),
|
||||||
STEEL (R.color.conversation_steel, R.color.conversation_steel_tint, R.color.conversation_steel_shade, "grey"),
|
STEEL (R.color.conversation_steel, R.color.conversation_steel_tint, R.color.conversation_steel_shade, "grey"),
|
||||||
|
ULTRAMARINE(R.color.conversation_ultramarine, R.color.conversation_ultramarine_tint, R.color.conversation_ultramarine_shade, "ultramarine"),
|
||||||
GROUP (R.color.conversation_group, R.color.conversation_group_tint, R.color.conversation_group_shade, "blue");
|
GROUP (R.color.conversation_group, R.color.conversation_group_tint, R.color.conversation_group_shade, "blue");
|
||||||
|
|
||||||
private static final Map<String, MaterialColor> COLOR_MATCHES = new HashMap<String, MaterialColor>() {{
|
private static final Map<String, MaterialColor> COLOR_MATCHES = new HashMap<String, MaterialColor>() {{
|
||||||
@@ -48,6 +50,7 @@ public enum MaterialColor {
|
|||||||
put("lime", WINTERGREEN);
|
put("lime", WINTERGREEN);
|
||||||
put("blue_grey", TAUPE);
|
put("blue_grey", TAUPE);
|
||||||
put("grey", STEEL);
|
put("grey", STEEL);
|
||||||
|
put("ultramarine", ULTRAMARINE);
|
||||||
put("group_color", GROUP);
|
put("group_color", GROUP);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
|||||||
@@ -11,18 +11,19 @@ import java.util.List;
|
|||||||
public class MaterialColors {
|
public class MaterialColors {
|
||||||
|
|
||||||
public static final MaterialColorList CONVERSATION_PALETTE = new MaterialColorList(new ArrayList<>(Arrays.asList(
|
public static final MaterialColorList CONVERSATION_PALETTE = new MaterialColorList(new ArrayList<>(Arrays.asList(
|
||||||
MaterialColor.PLUM,
|
MaterialColor.PLUM,
|
||||||
MaterialColor.CRIMSON,
|
MaterialColor.CRIMSON,
|
||||||
MaterialColor.VERMILLION,
|
MaterialColor.VERMILLION,
|
||||||
MaterialColor.VIOLET,
|
MaterialColor.VIOLET,
|
||||||
MaterialColor.BLUE,
|
MaterialColor.INDIGO,
|
||||||
MaterialColor.INDIGO,
|
MaterialColor.TAUPE,
|
||||||
MaterialColor.FOREST,
|
MaterialColor.ULTRAMARINE,
|
||||||
MaterialColor.WINTERGREEN,
|
MaterialColor.BLUE,
|
||||||
MaterialColor.TEAL,
|
MaterialColor.TEAL,
|
||||||
MaterialColor.BURLAP,
|
MaterialColor.FOREST,
|
||||||
MaterialColor.TAUPE,
|
MaterialColor.WINTERGREEN,
|
||||||
MaterialColor.STEEL
|
MaterialColor.BURLAP,
|
||||||
|
MaterialColor.STEEL
|
||||||
)));
|
)));
|
||||||
|
|
||||||
public static class MaterialColorList {
|
public static class MaterialColorList {
|
||||||
@@ -61,9 +62,6 @@ public class MaterialColors {
|
|||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,14 @@ package org.thoughtcrime.securesms.components;
|
|||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.ToggleButton;
|
|
||||||
|
|
||||||
public class AccessibleToggleButton extends ToggleButton {
|
import androidx.appcompat.widget.AppCompatToggleButton;
|
||||||
|
|
||||||
|
public class AccessibleToggleButton extends AppCompatToggleButton {
|
||||||
|
|
||||||
private OnCheckedChangeListener listener;
|
private OnCheckedChangeListener listener;
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public AccessibleToggleButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccessibleToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
public AccessibleToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import android.graphics.Rect;
|
|||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ import org.greenrobot.eventbus.Subscribe;
|
|||||||
import org.greenrobot.eventbus.ThreadMode;
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||||
|
import org.thoughtcrime.securesms.audio.AudioWaveForm;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
@@ -36,7 +37,7 @@ import org.thoughtcrime.securesms.mms.AudioSlide;
|
|||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Locale;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public final class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
|
public final class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
|
||||||
@@ -47,7 +48,6 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
private static final int REVERSE = -1;
|
private static final int REVERSE = -1;
|
||||||
|
|
||||||
@NonNull private final AnimatingToggle controlToggle;
|
@NonNull private final AnimatingToggle controlToggle;
|
||||||
@NonNull private final ViewGroup container;
|
|
||||||
@NonNull private final View progressAndPlay;
|
@NonNull private final View progressAndPlay;
|
||||||
@NonNull private final LottieAnimationView playPauseButton;
|
@NonNull private final LottieAnimationView playPauseButton;
|
||||||
@NonNull private final ImageView downloadButton;
|
@NonNull private final ImageView downloadButton;
|
||||||
@@ -56,13 +56,17 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
private final boolean smallView;
|
private final boolean smallView;
|
||||||
private final boolean autoRewind;
|
private final boolean autoRewind;
|
||||||
|
|
||||||
@Nullable private final TextView timestamp;
|
@Nullable private final TextView duration;
|
||||||
|
|
||||||
|
@ColorInt private final int waveFormPlayedBarsColor;
|
||||||
|
@ColorInt private final int waveFormUnplayedBarsColor;
|
||||||
|
|
||||||
@Nullable private SlideClickListener downloadListener;
|
@Nullable private SlideClickListener downloadListener;
|
||||||
@Nullable private AudioSlidePlayer audioSlidePlayer;
|
@Nullable private AudioSlidePlayer audioSlidePlayer;
|
||||||
private int backwardsCounter;
|
private int backwardsCounter;
|
||||||
private int lottieDirection;
|
private int lottieDirection;
|
||||||
private boolean isPlaying;
|
private boolean isPlaying;
|
||||||
|
private long durationMillis;
|
||||||
|
|
||||||
public AudioView(Context context) {
|
public AudioView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@@ -83,22 +87,22 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
|
|
||||||
inflate(context, smallView ? R.layout.audio_view_small : R.layout.audio_view, this);
|
inflate(context, smallView ? R.layout.audio_view_small : R.layout.audio_view, this);
|
||||||
|
|
||||||
this.container = findViewById(R.id.audio_widget_container);
|
|
||||||
this.controlToggle = findViewById(R.id.control_toggle);
|
this.controlToggle = findViewById(R.id.control_toggle);
|
||||||
this.playPauseButton = findViewById(R.id.play);
|
this.playPauseButton = findViewById(R.id.play);
|
||||||
this.progressAndPlay = findViewById(R.id.progress_and_play);
|
this.progressAndPlay = findViewById(R.id.progress_and_play);
|
||||||
this.downloadButton = findViewById(R.id.download);
|
this.downloadButton = findViewById(R.id.download);
|
||||||
this.circleProgress = findViewById(R.id.circle_progress);
|
this.circleProgress = findViewById(R.id.circle_progress);
|
||||||
this.seekBar = findViewById(R.id.seek);
|
this.seekBar = findViewById(R.id.seek);
|
||||||
this.timestamp = findViewById(R.id.timestamp);
|
this.duration = findViewById(R.id.duration);
|
||||||
|
|
||||||
lottieDirection = REVERSE;
|
lottieDirection = REVERSE;
|
||||||
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
||||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||||
|
|
||||||
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE),
|
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
|
||||||
typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.WHITE));
|
|
||||||
container.setBackgroundColor(typedArray.getColor(R.styleable.AudioView_widgetBackground, Color.TRANSPARENT));
|
this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
|
||||||
|
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
|
||||||
} finally {
|
} finally {
|
||||||
if (typedArray != null) {
|
if (typedArray != null) {
|
||||||
typedArray.recycle();
|
typedArray.recycle();
|
||||||
@@ -121,6 +125,14 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
public void setAudio(final @NonNull AudioSlide audio,
|
public void setAudio(final @NonNull AudioSlide audio,
|
||||||
final boolean showControls)
|
final boolean showControls)
|
||||||
{
|
{
|
||||||
|
if (seekBar instanceof WaveFormSeekBarView) {
|
||||||
|
if (audioSlidePlayer != null && !Objects.equals(audioSlidePlayer.getAudioSlide().getUri(), audio.getUri())) {
|
||||||
|
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||||
|
waveFormView.setWaveMode(false);
|
||||||
|
seekBar.setProgress(0);
|
||||||
|
durationMillis = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showControls && audio.isPendingDownload()) {
|
if (showControls && audio.isPendingDownload()) {
|
||||||
controlToggle.displayQuick(downloadButton);
|
controlToggle.displayQuick(downloadButton);
|
||||||
@@ -141,6 +153,28 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
|
this.audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
|
||||||
|
|
||||||
|
if (seekBar instanceof WaveFormSeekBarView) {
|
||||||
|
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||||
|
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor);
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||||
|
new AudioWaveForm(getContext(), audio).getWaveForm(
|
||||||
|
data -> {
|
||||||
|
if (duration != null) {
|
||||||
|
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||||
|
updateProgress(0, 0);
|
||||||
|
duration.setVisibility(VISIBLE);
|
||||||
|
}
|
||||||
|
waveFormView.setWaveData(data.getWaveForm());
|
||||||
|
},
|
||||||
|
() -> waveFormView.setWaveMode(false));
|
||||||
|
} else {
|
||||||
|
waveFormView.setWaveMode(false);
|
||||||
|
if (duration != null) {
|
||||||
|
duration.setVisibility(GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
@@ -210,10 +244,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateProgress(float progress, long millis) {
|
private void updateProgress(float progress, long millis) {
|
||||||
if (timestamp != null) {
|
if (duration != null && durationMillis > 0) {
|
||||||
timestamp.setText(String.format(Locale.getDefault(), "%02d:%02d",
|
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
|
||||||
TimeUnit.MILLISECONDS.toMinutes(millis),
|
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||||
TimeUnit.MILLISECONDS.toSeconds(millis)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smallView) {
|
if (smallView) {
|
||||||
@@ -221,7 +254,7 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTint(int foregroundTint, int backgroundTint) {
|
public void setTint(int foregroundTint) {
|
||||||
post(()-> this.playPauseButton.addValueCallback(new KeyPath("**"),
|
post(()-> this.playPauseButton.addValueCallback(new KeyPath("**"),
|
||||||
LottieProperty.COLOR_FILTER,
|
LottieProperty.COLOR_FILTER,
|
||||||
new LottieValueCallback<>(new SimpleColorFilter(foregroundTint))));
|
new LottieValueCallback<>(new SimpleColorFilter(foregroundTint))));
|
||||||
@@ -229,8 +262,8 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||||
this.circleProgress.setBarColor(foregroundTint);
|
this.circleProgress.setBarColor(foregroundTint);
|
||||||
|
|
||||||
if (this.timestamp != null) {
|
if (this.duration != null) {
|
||||||
this.timestamp.setTextColor(foregroundTint);
|
this.duration.setTextColor(foregroundTint);
|
||||||
}
|
}
|
||||||
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||||
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||||
@@ -336,7 +369,12 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||||||
private boolean wasPlaying;
|
private boolean wasPlaying;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
if (fromUser && durationMillis > 0) {
|
||||||
|
float progressFloat = progress / (float) seekBar.getMax();
|
||||||
|
updateProgress(progressFloat, (long) (durationMillis * progressFloat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
|
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -16,12 +15,17 @@ import androidx.appcompat.widget.AppCompatImageView;
|
|||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.RecipientPreferenceActivity;
|
||||||
|
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -49,10 +53,11 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
DARK_THEME_OUTLINE_PAINT.setAntiAlias(true);
|
DARK_THEME_OUTLINE_PAINT.setAntiAlias(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int size;
|
private int size;
|
||||||
private boolean inverted;
|
private boolean inverted;
|
||||||
private Paint outlinePaint;
|
private Paint outlinePaint;
|
||||||
private OnClickListener listener;
|
private OnClickListener listener;
|
||||||
|
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
|
||||||
|
|
||||||
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
||||||
private @NonNull Drawable unknownRecipientDrawable;
|
private @NonNull Drawable unknownRecipientDrawable;
|
||||||
@@ -102,6 +107,19 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
super.setOnClickListener(listener);
|
super.setOnClickListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFallbackPhotoProvider(Recipient.FallbackPhotoProvider fallbackPhotoProvider) {
|
||||||
|
this.fallbackPhotoProvider = fallbackPhotoProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecipient(@NonNull Recipient recipient) {
|
||||||
|
if (recipient.isLocalNumber()) {
|
||||||
|
setAvatar(GlideApp.with(this), null, false);
|
||||||
|
AvatarUtil.loadIconIntoImageView(recipient, this);
|
||||||
|
} else {
|
||||||
|
setAvatar(GlideApp.with(this), recipient, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
|
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
|
||||||
if (recipient != null) {
|
if (recipient != null) {
|
||||||
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
|
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
|
||||||
@@ -111,8 +129,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
recipientContactPhoto = photo;
|
recipientContactPhoto = photo;
|
||||||
|
|
||||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL
|
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL
|
||||||
? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted)
|
? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
|
||||||
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted);
|
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
|
||||||
|
|
||||||
if (photo.contactPhoto != null) {
|
if (photo.contactPhoto != null) {
|
||||||
requestManager.load(photo.contactPhoto)
|
requestManager.load(photo.contactPhoto)
|
||||||
@@ -130,23 +148,31 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
} else {
|
} else {
|
||||||
recipientContactPhoto = null;
|
recipientContactPhoto = null;
|
||||||
requestManager.clear(this);
|
requestManager.clear(this);
|
||||||
setImageDrawable(unknownRecipientDrawable);
|
if (fallbackPhotoProvider != null) {
|
||||||
|
setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
|
||||||
|
.asDrawable(getContext(), MaterialColor.STEEL.toAvatarColor(getContext()), inverted));
|
||||||
|
} else {
|
||||||
|
setImageDrawable(unknownRecipientDrawable);
|
||||||
|
}
|
||||||
|
|
||||||
super.setOnClickListener(listener);
|
super.setOnClickListener(listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAvatarClickHandler(final Recipient recipient, boolean quickContactEnabled) {
|
private void setAvatarClickHandler(@NonNull final Recipient recipient, boolean quickContactEnabled) {
|
||||||
super.setOnClickListener(v -> {
|
if (quickContactEnabled) {
|
||||||
if (!recipient.isGroup() && quickContactEnabled) {
|
super.setOnClickListener(v -> {
|
||||||
if (recipient.getContactUri() != null) {
|
if (FeatureFlags.newGroupUI() && recipient.isPushGroup()) {
|
||||||
ContactsContract.QuickContact.showQuickContact(getContext(), AvatarImageView.this, recipient.getContactUri(), ContactsContract.QuickContact.MODE_LARGE, null);
|
getContext().startActivity(ManageGroupActivity.newIntent(getContext(), recipient.requireGroupId().requirePush()),
|
||||||
|
ManageGroupActivity.createTransitionBundle(getContext(), this));
|
||||||
} else {
|
} else {
|
||||||
getContext().startActivity(RecipientExporter.export(recipient).asAddContactIntent());
|
getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId()));
|
||||||
}
|
}
|
||||||
} else if (listener != null) {
|
});
|
||||||
listener.onClick(v);
|
} else {
|
||||||
}
|
super.setOnClickListener(listener);
|
||||||
});
|
setClickable(listener != null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class RecipientContactPhoto {
|
private static class RecipientContactPhoto {
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ import android.widget.LinearLayout;
|
|||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar;
|
||||||
|
|
||||||
public class ContactFilterToolbar extends Toolbar {
|
public class ContactFilterToolbar extends DarkOverflowToolbar {
|
||||||
private OnFilterChangedListener listener;
|
private OnFilterChangedListener listener;
|
||||||
|
|
||||||
private EditText searchText;
|
private EditText searchText;
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import androidx.annotation.DimenRes;
|
|
||||||
import androidx.annotation.MainThread;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.view.animation.AlphaAnimation;
|
import android.view.animation.AlphaAnimation;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationSet;
|
import android.view.animation.AnimationSet;
|
||||||
@@ -24,7 +20,16 @@ import android.widget.LinearLayout;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.DimenRes;
|
||||||
|
import androidx.annotation.MainThread;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||||
@@ -57,7 +62,8 @@ public class InputPanel extends LinearLayout
|
|||||||
|
|
||||||
private static final String TAG = InputPanel.class.getSimpleName();
|
private static final String TAG = InputPanel.class.getSimpleName();
|
||||||
|
|
||||||
private static final int FADE_TIME = 150;
|
private static final long QUOTE_REVEAL_DURATION_MILLIS = 150;
|
||||||
|
private static final int FADE_TIME = 150;
|
||||||
|
|
||||||
private RecyclerView stickerSuggestion;
|
private RecyclerView stickerSuggestion;
|
||||||
private QuoteView quoteView;
|
private QuoteView quoteView;
|
||||||
@@ -73,6 +79,7 @@ public class InputPanel extends LinearLayout
|
|||||||
private MicrophoneRecorderView microphoneRecorderView;
|
private MicrophoneRecorderView microphoneRecorderView;
|
||||||
private SlideToCancel slideToCancel;
|
private SlideToCancel slideToCancel;
|
||||||
private RecordTime recordTime;
|
private RecordTime recordTime;
|
||||||
|
private ValueAnimator quoteAnimator;
|
||||||
|
|
||||||
private @Nullable Listener listener;
|
private @Nullable Listener listener;
|
||||||
private boolean emojiVisible;
|
private boolean emojiVisible;
|
||||||
@@ -157,7 +164,20 @@ public class InputPanel extends LinearLayout
|
|||||||
@NonNull SlideDeck attachments)
|
@NonNull SlideDeck attachments)
|
||||||
{
|
{
|
||||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
|
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
|
||||||
this.quoteView.setVisibility(View.VISIBLE);
|
|
||||||
|
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
this.quoteView.setVisibility(VISIBLE);
|
||||||
|
this.quoteView.measure(0, 0);
|
||||||
|
|
||||||
|
if (quoteAnimator != null) {
|
||||||
|
quoteAnimator.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteAnimator = createHeightAnimator(quoteView, originalHeight, this.quoteView.getMeasuredHeight(), null);
|
||||||
|
|
||||||
|
quoteAnimator.start();
|
||||||
|
|
||||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
||||||
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
|
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
|
||||||
@@ -166,12 +186,44 @@ public class InputPanel extends LinearLayout
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void clearQuote() {
|
public void clearQuote() {
|
||||||
this.quoteView.dismiss();
|
if (quoteAnimator != null) {
|
||||||
|
quoteAnimator.cancel();
|
||||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
|
||||||
int cornerRadius = readDimen(R.dimen.message_corner_radius);
|
|
||||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quoteAnimator = createHeightAnimator(quoteView, quoteView.getMeasuredHeight(), 0, new AnimationCompleteListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
quoteView.dismiss();
|
||||||
|
|
||||||
|
if (linkPreview.getVisibility() == View.VISIBLE) {
|
||||||
|
int cornerRadius = readDimen(R.dimen.message_corner_radius);
|
||||||
|
linkPreview.setCorners(cornerRadius, cornerRadius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quoteAnimator.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValueAnimator createHeightAnimator(@NonNull View view,
|
||||||
|
int originalHeight,
|
||||||
|
int finalHeight,
|
||||||
|
@Nullable AnimationCompleteListener onAnimationComplete)
|
||||||
|
{
|
||||||
|
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, finalHeight)
|
||||||
|
.setDuration(QUOTE_REVEAL_DURATION_MILLIS);
|
||||||
|
|
||||||
|
animator.addUpdateListener(animation -> {
|
||||||
|
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||||
|
params.height = (int) animation.getAnimatedValue();
|
||||||
|
view.setLayoutParams(params);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onAnimationComplete != null) {
|
||||||
|
animator.addListener(onAnimationComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
return animator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<QuoteModel> getQuote() {
|
public Optional<QuoteModel> getQuote() {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.widget.HorizontalScrollView;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfortunately {@link HorizontalScrollView#setOnScrollChangeListener(OnScrollChangeListener)}
|
||||||
|
* wasn't added until API 23, so now we have to do this ourselves.
|
||||||
|
*/
|
||||||
|
public class ListenableHorizontalScrollView extends HorizontalScrollView {
|
||||||
|
|
||||||
|
private OnScrollListener listener;
|
||||||
|
|
||||||
|
public ListenableHorizontalScrollView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableHorizontalScrollView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnScrollListener(@Nullable OnScrollListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onScrollChanged(int newLeft, int newTop, int oldLeft, int oldTop) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onScroll(newLeft, oldLeft);
|
||||||
|
}
|
||||||
|
super.onScrollChanged(newLeft, newTop, oldLeft, oldTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnScrollListener {
|
||||||
|
void onScroll(int newLeft, int oldLeft);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ public class MaskView extends View {
|
|||||||
private ViewGroup activityContentView;
|
private ViewGroup activityContentView;
|
||||||
private Paint maskPaint;
|
private Paint maskPaint;
|
||||||
private Rect drawingRect = new Rect();
|
private Rect drawingRect = new Rect();
|
||||||
|
private float targetParentTranslationY;
|
||||||
|
|
||||||
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
|
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
|
||||||
|
|
||||||
@@ -63,6 +64,10 @@ public class MaskView extends View {
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTargetParentTranslationY(float targetParentTranslationY) {
|
||||||
|
this.targetParentTranslationY = targetParentTranslationY;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDraw(@NonNull Canvas canvas) {
|
protected void onDraw(@NonNull Canvas canvas) {
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
@@ -75,6 +80,8 @@ public class MaskView extends View {
|
|||||||
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
|
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
|
||||||
|
|
||||||
drawingRect.bottom = Math.min(drawingRect.bottom, getBottom() - getPaddingBottom());
|
drawingRect.bottom = Math.min(drawingRect.bottom, getBottom() - getPaddingBottom());
|
||||||
|
drawingRect.top += targetParentTranslationY;
|
||||||
|
drawingRect.bottom += targetParentTranslationY;
|
||||||
|
|
||||||
Bitmap mask = Bitmap.createBitmap(target.getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
|
Bitmap mask = Bitmap.createBitmap(target.getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
|
||||||
Canvas maskCanvas = new Canvas(mask);
|
Canvas maskCanvas = new Canvas(mask);
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import android.content.res.TypedArray;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -17,6 +14,10 @@ import android.widget.FrameLayout;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
|
||||||
@@ -248,6 +249,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||||||
}
|
}
|
||||||
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
|
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
|
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
.into(thumbnailView);
|
.into(thumbnailView);
|
||||||
} else if (!documentSlides.isEmpty()){
|
} else if (!documentSlides.isEmpty()){
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ public final class RecyclerViewFastScroller extends LinearLayout {
|
|||||||
final int action = event.getAction();
|
final int action = event.getAction();
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
if (event.getX() < ViewUtil.getX(handle) - handle.getPaddingLeft() ||
|
if (event.getX() < handle.getX() - handle.getPaddingLeft() ||
|
||||||
event.getY() < ViewUtil.getY(handle) - handle.getPaddingTop() ||
|
event.getY() < handle.getY() - handle.getPaddingTop() ||
|
||||||
event.getY() > ViewUtil.getY(handle) + handle.getHeight() + handle.getPaddingBottom())
|
event.getY() > handle.getY() + handle.getHeight() + handle.getPaddingBottom())
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -150,9 +150,9 @@ public final class RecyclerViewFastScroller extends LinearLayout {
|
|||||||
if (recyclerView != null) {
|
if (recyclerView != null) {
|
||||||
final int itemCount = recyclerView.getAdapter().getItemCount();
|
final int itemCount = recyclerView.getAdapter().getItemCount();
|
||||||
float proportion;
|
float proportion;
|
||||||
if (ViewUtil.getY(handle) == 0) {
|
if (handle.getY() == 0) {
|
||||||
proportion = 0f;
|
proportion = 0f;
|
||||||
} else if (ViewUtil.getY(handle) + handle.getHeight() >= height - TRACK_SNAP_RANGE) {
|
} else if (handle.getY() + handle.getHeight() >= height - TRACK_SNAP_RANGE) {
|
||||||
proportion = 1f;
|
proportion = 1f;
|
||||||
} else {
|
} else {
|
||||||
proportion = y / (float)height;
|
proportion = y / (float)height;
|
||||||
@@ -169,10 +169,10 @@ public final class RecyclerViewFastScroller extends LinearLayout {
|
|||||||
final int handleHeight = handle.getHeight();
|
final int handleHeight = handle.getHeight();
|
||||||
final int bubbleHeight = bubble.getHeight();
|
final int bubbleHeight = bubble.getHeight();
|
||||||
final int handleY = Util.clamp((int)((height - handleHeight) * y), 0, height - handleHeight);
|
final int handleY = Util.clamp((int)((height - handleHeight) * y), 0, height - handleHeight);
|
||||||
ViewUtil.setY(handle, handleY);
|
handle.setY(handleY);
|
||||||
ViewUtil.setY(bubble, Util.clamp(handleY - bubbleHeight - bubble.getPaddingBottom() + handleHeight,
|
bubble.setY(Util.clamp(handleY - bubbleHeight - bubble.getPaddingBottom() + handleHeight,
|
||||||
0,
|
0,
|
||||||
height - bubbleHeight));
|
height - bubbleHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showBubble() {
|
private void showBubble() {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import com.annimon.stream.Stream;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.animation.Interpolator;
|
||||||
|
import android.view.animation.OvershootInterpolator;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.Px;
|
||||||
|
import androidx.appcompat.widget.AppCompatSeekBar;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public final class WaveFormSeekBarView extends AppCompatSeekBar {
|
||||||
|
|
||||||
|
private static final int ANIM_DURATION = 450;
|
||||||
|
private static final int ANIM_BAR_OFF_SET_DURATION = 12;
|
||||||
|
|
||||||
|
private final Interpolator overshoot = new OvershootInterpolator();
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
private float[] data = new float[0];
|
||||||
|
private long dataSetTime;
|
||||||
|
private Drawable progressDrawable;
|
||||||
|
private boolean waveMode;
|
||||||
|
|
||||||
|
@ColorInt private int playedBarColor = 0xffffffff;
|
||||||
|
@ColorInt private int unplayedBarColor = 0x7fffffff;
|
||||||
|
@Px private int barWidth;
|
||||||
|
|
||||||
|
public WaveFormSeekBarView(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaveFormSeekBarView(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaveFormSeekBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
setWillNotDraw(false);
|
||||||
|
|
||||||
|
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
|
||||||
|
progressDrawable = super.getProgressDrawable();
|
||||||
|
|
||||||
|
if (isInEditMode()) {
|
||||||
|
setWaveData(sinusoidalExampleData());
|
||||||
|
dataSetTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
barWidth = getResources().getDimensionPixelSize(R.dimen.wave_form_bar_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColors(@ColorInt int playedBarColor, @ColorInt int unplayedBarColor) {
|
||||||
|
this.playedBarColor = playedBarColor;
|
||||||
|
this.unplayedBarColor = unplayedBarColor;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProgressDrawable(Drawable progressDrawable) {
|
||||||
|
this.progressDrawable = progressDrawable;
|
||||||
|
if (!waveMode) {
|
||||||
|
super.setProgressDrawable(progressDrawable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Drawable getProgressDrawable() {
|
||||||
|
return progressDrawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWaveData(@NonNull float[] data) {
|
||||||
|
if (!Arrays.equals(data, this.data)) {
|
||||||
|
this.data = data;
|
||||||
|
this.dataSetTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
setWaveMode(data.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWaveMode(boolean waveMode) {
|
||||||
|
this.waveMode = waveMode;
|
||||||
|
super.setProgressDrawable(this.waveMode ? null : progressDrawable);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
if (waveMode) {
|
||||||
|
drawWave(canvas);
|
||||||
|
}
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawWave(Canvas canvas) {
|
||||||
|
paint.setStrokeWidth(barWidth);
|
||||||
|
|
||||||
|
int usableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
|
||||||
|
int usableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
|
||||||
|
float midpoint = usableHeight / 2f;
|
||||||
|
float maxHeight = usableHeight / 2f - barWidth;
|
||||||
|
float barGap = (usableWidth - data.length * barWidth) / (float) (data.length - 1);
|
||||||
|
|
||||||
|
boolean hasMoreFrames = false;
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(getPaddingLeft(), getPaddingTop());
|
||||||
|
|
||||||
|
for (int bar = 0; bar < data.length; bar++) {
|
||||||
|
float x = bar * (barWidth + barGap) + barWidth / 2f;
|
||||||
|
float y = data[bar] * maxHeight;
|
||||||
|
float progress = x / usableWidth;
|
||||||
|
|
||||||
|
paint.setColor(progress * getMax() < getProgress() ? playedBarColor : unplayedBarColor);
|
||||||
|
|
||||||
|
long time = System.currentTimeMillis() - bar * ANIM_BAR_OFF_SET_DURATION - dataSetTime;
|
||||||
|
float timeX = Math.max(0, Math.min(1, time / (float) ANIM_DURATION));
|
||||||
|
float interpolatedTime = overshoot.getInterpolation(timeX);
|
||||||
|
float interpolatedY = y * interpolatedTime;
|
||||||
|
|
||||||
|
canvas.drawLine(x, midpoint - interpolatedY, x, midpoint + interpolatedY, paint);
|
||||||
|
|
||||||
|
if (time < ANIM_DURATION) {
|
||||||
|
hasMoreFrames = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
|
if (hasMoreFrames) {
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[] sinusoidalExampleData() {
|
||||||
|
float[] data = new float[21];
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
data[i] = (float) Math.sin(i / (float) (data.length - 1) * 2 * Math.PI);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
|||||||
{
|
{
|
||||||
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
|
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
|
||||||
|
|
||||||
|
private static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final List<EmojiPageModel> models;
|
private final List<EmojiPageModel> models;
|
||||||
private final RecentEmojiPageModel recentModel;
|
private final RecentEmojiPageModel recentModel;
|
||||||
@@ -41,7 +43,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
this.emojiEventListener = emojiEventListener;
|
this.emojiEventListener = emojiEventListener;
|
||||||
this.models = new LinkedList<>();
|
this.models = new LinkedList<>();
|
||||||
this.recentModel = new RecentEmojiPageModel(context);
|
this.recentModel = new RecentEmojiPageModel(context, RECENT_STORAGE_KEY);
|
||||||
this.emojiPagerAdapter = new EmojiPagerAdapter(context, models, new EmojiEventListener() {
|
this.emojiPagerAdapter = new EmojiPagerAdapter(context, models, new EmojiEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onEmojiSelected(String emoji) {
|
public void onEmojiSelected(String emoji) {
|
||||||
@@ -133,7 +135,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||||
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener);
|
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, true);
|
||||||
page.setModel(pages.get(position));
|
page.setModel(pages.get(position));
|
||||||
container.addView(page);
|
container.addView(page);
|
||||||
return page;
|
return page;
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
|||||||
|
|
||||||
public EmojiPageView(@NonNull Context context,
|
public EmojiPageView(@NonNull Context context,
|
||||||
@NonNull EmojiEventListener emojiSelectionListener,
|
@NonNull EmojiEventListener emojiSelectionListener,
|
||||||
@NonNull VariationSelectorListener variationSelectorListener)
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
|
boolean allowVariations)
|
||||||
{
|
{
|
||||||
super(context);
|
super(context);
|
||||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
|
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
|
||||||
@@ -40,7 +41,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
|||||||
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
|
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
|
||||||
popup,
|
popup,
|
||||||
emojiSelectionListener,
|
emojiSelectionListener,
|
||||||
this);
|
this,
|
||||||
|
allowVariations);
|
||||||
|
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
@@ -83,6 +85,10 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
|
||||||
|
recyclerView.setNestedScrollingEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
|
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
|
||||||
@Override
|
@Override
|
||||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
|
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
|
||||||
|
|||||||
@@ -22,17 +22,20 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
|
|||||||
private final EmojiVariationSelectorPopup popup;
|
private final EmojiVariationSelectorPopup popup;
|
||||||
private final VariationSelectorListener variationSelectorListener;
|
private final VariationSelectorListener variationSelectorListener;
|
||||||
private final EmojiEventListener emojiEventListener;
|
private final EmojiEventListener emojiEventListener;
|
||||||
|
private final boolean allowVariations;
|
||||||
|
|
||||||
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
|
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
|
||||||
@NonNull EmojiVariationSelectorPopup popup,
|
@NonNull EmojiVariationSelectorPopup popup,
|
||||||
@NonNull EmojiEventListener emojiEventListener,
|
@NonNull EmojiEventListener emojiEventListener,
|
||||||
@NonNull VariationSelectorListener variationSelectorListener)
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
|
boolean allowVariations)
|
||||||
{
|
{
|
||||||
this.emojiList = new ArrayList<>();
|
this.emojiList = new ArrayList<>();
|
||||||
this.emojiProvider = emojiProvider;
|
this.emojiProvider = emojiProvider;
|
||||||
this.popup = popup;
|
this.popup = popup;
|
||||||
this.emojiEventListener = emojiEventListener;
|
this.emojiEventListener = emojiEventListener;
|
||||||
this.variationSelectorListener = variationSelectorListener;
|
this.variationSelectorListener = variationSelectorListener;
|
||||||
|
this.allowVariations = allowVariations;
|
||||||
|
|
||||||
popup.setOnDismissListener(this);
|
popup.setOnDismissListener(this);
|
||||||
}
|
}
|
||||||
@@ -65,7 +68,7 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
|
|||||||
emojiEventListener.onEmojiSelected(emoji.getValue());
|
emojiEventListener.onEmojiSelected(emoji.getValue());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (emoji.getVariations().size() > 1) {
|
if (allowVariations && emoji.getVariations().size() > 1) {
|
||||||
viewHolder.itemView.setOnLongClickListener(v -> {
|
viewHolder.itemView.setOnLongClickListener(v -> {
|
||||||
popup.dismiss();
|
popup.dismiss();
|
||||||
popup.setVariations(emoji.getVariations());
|
popup.setVariations(emoji.getVariations());
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class EmojiProvider {
|
|||||||
private static final int EMOJI_RAW_HEIGHT = 64;
|
private static final int EMOJI_RAW_HEIGHT = 64;
|
||||||
private static final int EMOJI_RAW_WIDTH = 64;
|
private static final int EMOJI_RAW_WIDTH = 64;
|
||||||
private static final int EMOJI_VERT_PAD = 0;
|
private static final int EMOJI_VERT_PAD = 0;
|
||||||
private static final int EMOJI_PER_ROW = 32;
|
private static final int EMOJI_PER_ROW = 16;
|
||||||
|
|
||||||
private final float decodeScale;
|
private final float decodeScale;
|
||||||
private final float verticalPad;
|
private final float verticalPad;
|
||||||
|
|||||||
@@ -2,4 +2,10 @@ package org.thoughtcrime.securesms.components.emoji;
|
|||||||
|
|
||||||
public final class EmojiStrings {
|
public final class EmojiStrings {
|
||||||
public static final String BUST_IN_SILHOUETTE = "\uD83D\uDC64";
|
public static final String BUST_IN_SILHOUETTE = "\uD83D\uDC64";
|
||||||
|
public static final String PHOTO = "\uD83D\uDCF7";
|
||||||
|
public static final String VIDEO = "\uD83C\uDFA5";
|
||||||
|
public static final String GIF = "\uD83C\uDFA1";
|
||||||
|
public static final String AUDIO = "\uD83C\uDFA4";
|
||||||
|
public static final String FILE = "\uD83D\uDCCE";
|
||||||
|
public static final String STICKER = "\u2B50";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class EmojiUtil {
|
||||||
|
|
||||||
|
private static final Map<String, String> VARIATION_MAP = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
|
||||||
|
for (Emoji emoji : page.getDisplayEmoji()) {
|
||||||
|
for (String variation : emoji.getVariations()) {
|
||||||
|
VARIATION_MAP.put(variation, emoji.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int MAX_EMOJI_LENGTH;
|
||||||
|
static {
|
||||||
|
int max = 0;
|
||||||
|
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
|
||||||
|
for (String emoji : page.getEmoji()) {
|
||||||
|
max = Math.max(max, emoji.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MAX_EMOJI_LENGTH = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EmojiUtil() {}
|
||||||
|
|
||||||
|
public static List<EmojiPageModel> getDisplayPages() {
|
||||||
|
return EmojiPages.DISPLAY_PAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will return all ways we know of expressing a singular emoji. This is to aid in search,
|
||||||
|
* where some platforms may send an emoji we've locally marked as 'obsolete'.
|
||||||
|
*/
|
||||||
|
public static @NonNull Set<String> getAllRepresentations(@NonNull String emoji) {
|
||||||
|
Set<String> out = new HashSet<>();
|
||||||
|
|
||||||
|
out.add(emoji);
|
||||||
|
|
||||||
|
for (Pair<String, String> pair : EmojiPages.OBSOLETE) {
|
||||||
|
if (pair.first().equals(emoji)) {
|
||||||
|
out.add(pair.second());
|
||||||
|
} else if (pair.second().equals(emoji)) {
|
||||||
|
out.add(pair.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When provided an emoji that is a skin variation of another, this will return the default yellow
|
||||||
|
* version. This is to aid in search, so using a variation will still find all emojis tagged with
|
||||||
|
* the default version.
|
||||||
|
*
|
||||||
|
* If the emoji has no skin variations, this function will return the original emoji.
|
||||||
|
*/
|
||||||
|
public static @NonNull String getCanonicalRepresentation(@NonNull String emoji) {
|
||||||
|
String canonical = VARIATION_MAP.get(emoji);
|
||||||
|
return canonical != null ? canonical : emoji;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow {
|
|||||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.list = (ViewGroup) getContentView();
|
this.list = (ViewGroup) getContentView().findViewById(R.id.emoji_variation_container);
|
||||||
|
|
||||||
setBackgroundDrawable(null);
|
setBackgroundDrawable(null);
|
||||||
setOutsideTouchable(true);
|
setOutsideTouchable(true);
|
||||||
|
|||||||
@@ -22,20 +22,21 @@ import java.util.LinkedHashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class RecentEmojiPageModel implements EmojiPageModel {
|
public class RecentEmojiPageModel implements EmojiPageModel {
|
||||||
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
||||||
private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji2";
|
private static final int EMOJI_LRU_SIZE = 50;
|
||||||
private static final int EMOJI_LRU_SIZE = 50;
|
|
||||||
|
|
||||||
private final SharedPreferences prefs;
|
private final SharedPreferences prefs;
|
||||||
|
private final String preferenceName;
|
||||||
private final LinkedHashSet<String> recentlyUsed;
|
private final LinkedHashSet<String> recentlyUsed;
|
||||||
|
|
||||||
public RecentEmojiPageModel(Context context) {
|
public RecentEmojiPageModel(Context context, @NonNull String preferenceName) {
|
||||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
this.recentlyUsed = getPersistedCache();
|
this.preferenceName = preferenceName;
|
||||||
|
this.recentlyUsed = getPersistedCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LinkedHashSet<String> getPersistedCache() {
|
private LinkedHashSet<String> getPersistedCache() {
|
||||||
String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]");
|
String serialized = prefs.getString(preferenceName, "[]");
|
||||||
try {
|
try {
|
||||||
CollectionType collectionType = TypeFactory.defaultInstance()
|
CollectionType collectionType = TypeFactory.defaultInstance()
|
||||||
.constructCollectionType(LinkedHashSet.class, String.class);
|
.constructCollectionType(LinkedHashSet.class, String.class);
|
||||||
@@ -90,7 +91,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
|||||||
try {
|
try {
|
||||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(EMOJI_LRU_PREFERENCE, serialized)
|
.putString(preferenceName, serialized)
|
||||||
.apply();
|
.apply();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.Switch;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.util.Consumer;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private final OnAudioOutputChangedListener onAudioOutputChangedListener;
|
||||||
|
private final List<WebRtcAudioOutput> audioOutputs;
|
||||||
|
|
||||||
|
private WebRtcAudioOutput selected;
|
||||||
|
|
||||||
|
AudioOutputAdapter(@NonNull OnAudioOutputChangedListener onAudioOutputChangedListener,
|
||||||
|
@NonNull List<WebRtcAudioOutput> audioOutputs) {
|
||||||
|
this.audioOutputs = audioOutputs;
|
||||||
|
this.onAudioOutputChangedListener = onAudioOutputChangedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedOutput(@NonNull WebRtcAudioOutput selected) {
|
||||||
|
this.selected = selected;
|
||||||
|
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.audio_output_adapter_radio_item, parent, false);
|
||||||
|
|
||||||
|
return new ViewHolder(view, this::handlePositionSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
holder.bind(audioOutputs.get(position), selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return audioOutputs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePositionSelected(int position) {
|
||||||
|
WebRtcAudioOutput mode = audioOutputs.get(position);
|
||||||
|
|
||||||
|
if (mode != selected) {
|
||||||
|
setSelectedOutput(mode);
|
||||||
|
onAudioOutputChangedListener.audioOutputChanged(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener {
|
||||||
|
|
||||||
|
private final TextView textView;
|
||||||
|
private final RadioButton radioButton;
|
||||||
|
private final Consumer<Integer> onPressed;
|
||||||
|
|
||||||
|
|
||||||
|
public ViewHolder(@NonNull View itemView, @NonNull Consumer<Integer> onPressed) {
|
||||||
|
super(itemView);
|
||||||
|
|
||||||
|
this.textView = itemView.findViewById(R.id.text);
|
||||||
|
this.radioButton = itemView.findViewById(R.id.radio);
|
||||||
|
this.onPressed = onPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void bind(@NonNull WebRtcAudioOutput audioOutput, @Nullable WebRtcAudioOutput selected) {
|
||||||
|
textView.setText(audioOutput.getLabelRes());
|
||||||
|
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
|
||||||
|
|
||||||
|
radioButton.setOnCheckedChangeListener(null);
|
||||||
|
radioButton.setChecked(audioOutput == selected);
|
||||||
|
radioButton.setOnCheckedChangeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
int adapterPosition = getAdapterPosition();
|
||||||
|
if (adapterPosition != RecyclerView.NO_POSITION) {
|
||||||
|
onPressed.accept(adapterPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||