Compare commits
549 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65e88d2d1c | |||
| cef8aa67dd | |||
| 5941b22eb6 | |||
| 9e7c55847e | |||
| 5209b74605 | |||
| b90a74d26a | |||
| 8c1737e597 | |||
| 2ea5bd2d44 | |||
| 4166e7931e | |||
| 89f2c25d73 | |||
| abb1ca2afe | |||
| f7befd1593 | |||
| 28511de23c | |||
| 2ff3d1b7c5 | |||
| fe6ae7e142 | |||
| 0da6c83ce4 | |||
| 184b7db43c | |||
| e442e34c1b | |||
| 011efb0ce7 | |||
| 63d00f87d8 | |||
| 0323858145 | |||
| a70e8ec7a7 | |||
| b306a3ef41 | |||
| ccd3467a61 | |||
| 40338afe7a | |||
| ff97f6af56 | |||
| 6e7858e00f | |||
| 95468c85a8 | |||
| f59e10d82c | |||
| 930370783e | |||
| 75062ada8a | |||
| 23618923d8 | |||
| f1d3a2f322 | |||
| 3b7fbbaf6e | |||
| 725d793b20 | |||
| 5c3baca055 | |||
| 6e5abc92a0 | |||
| 8df6e95781 | |||
| 2290a6c0df | |||
| 907e8d93a3 | |||
| 918497fb94 | |||
| 3ccd6304c7 | |||
| 51d47adf57 | |||
| f1e5206f56 | |||
| f410635e2c | |||
| 302d57bf19 | |||
| 4c301a49b4 | |||
| 4ecfee292e | |||
| 2a193ef455 | |||
| 96e241ef9c | |||
| e294a895e8 | |||
| 003b9b1551 | |||
| a4e4af502e | |||
| 06aada20c1 | |||
| 2dace38d43 | |||
| 554aa1ddf0 | |||
| 3b2a5f1ce3 | |||
| 3fc4b098e8 | |||
| a7d672f6b4 | |||
| 7e347f5cce | |||
| 4eaa6ebb47 | |||
| e85ef6881d | |||
| b1f6786392 | |||
| 696fffb603 | |||
| 3bb366ee04 | |||
| 6a59974f89 | |||
| 8e39267c42 | |||
| b937534ce5 | |||
| f5b46f7356 | |||
| cd58c09be3 | |||
| e8f0038c36 | |||
| 0b77b33902 | |||
| c3b5323010 | |||
| 81eaae4070 | |||
| 65461ce86f | |||
| 536e3139a2 | |||
| e9c7b120a0 | |||
| d6a230a235 | |||
| 6bf300ada8 | |||
| d307db8a95 | |||
| c4c32d80b2 | |||
| f4c1e34402 | |||
| 0068d62122 | |||
| 3f1fa59e09 | |||
| df5114c62c | |||
| 956e3924ff | |||
| 20ad166e0f | |||
| 12ea88f409 | |||
| 0c5648bfb1 | |||
| 91ca19f294 | |||
| 71250afd2c | |||
| cfdef7bca7 | |||
| 872f935fd5 | |||
| 0ed1f73990 | |||
| 349a2f72cb | |||
| 2b4a4d6109 | |||
| 9f882d2fbb | |||
| cb4a9730aa | |||
| e0657d09d8 | |||
| 01b9cb13b4 | |||
| 2c7260557c | |||
| 9e5156ab73 | |||
| 3dc1614fbc | |||
| 2f69a9c38e | |||
| 5e536c3fa5 | |||
| 6bb9d27d4e | |||
| 2d1bf33902 | |||
| 985a220fca | |||
| 31e137cf6d | |||
| f796447815 | |||
| 936e772ba0 | |||
| ecee797d00 | |||
| 357a8fc124 | |||
| 1233af0ddd | |||
| a264d10685 | |||
| ed17701a0a | |||
| 49e1ccea28 | |||
| 4c43b0d1e3 | |||
| 5ce09defca | |||
| da9064b714 | |||
| 7bb53e4b06 | |||
| 6a4ce1b658 | |||
| f84595e1e8 | |||
| 41d5c54033 | |||
| b9d6b63c09 | |||
| 506ad0b3f1 | |||
| c8302174a9 | |||
| 39cebfbb4e | |||
| d36ec9af47 | |||
| 5f6d971bf7 | |||
| 7a722d92a3 | |||
| 0bf0eba450 | |||
| d40783f794 | |||
| 88733473e2 | |||
| 7b65533095 | |||
| 52b533c121 | |||
| a4fa2e14fb | |||
| 6933f1d818 | |||
| b5d6cb2a8d | |||
| d1478c5ce0 | |||
| fbe62f0f3e | |||
| f84705b756 | |||
| cf2189c11a | |||
| dfc4178252 | |||
| 07952f2146 | |||
| a90dad22a9 | |||
| 64f7330609 | |||
| 5e382c120b | |||
| 3eea568f5f | |||
| 0077b29d6e | |||
| dfa6306b61 | |||
| a4bf075a1a | |||
| 373d622535 | |||
| ba1df58eb3 | |||
| 9fb85f7c76 | |||
| 5e58f0a212 | |||
| 8fa01f13e9 | |||
| 4ce136be17 | |||
| 4099154dc0 | |||
| 3f983a5c82 | |||
| 9743e3689a | |||
| 1363f55f77 | |||
| f1d98f6c7b | |||
| 9279a54d28 | |||
| 81889d8130 | |||
| 6aecb8fbc1 | |||
| 8aa413032d | |||
| 5bc4686eb8 | |||
| f676d1c61c | |||
| ac54b5cbdf | |||
| b4b1e5b605 | |||
| 5eace49739 | |||
| e93d7518f3 | |||
| 9c97cd8816 | |||
| 90f20c36c5 | |||
| 9f8dd7992a | |||
| f4d3fe9176 | |||
| ffc7c13717 | |||
| daf93c473b | |||
| d21782696a | |||
| 3357475fc4 | |||
| ead64d92a5 | |||
| 5eaac6cb17 | |||
| b3f0a44f10 | |||
| e4d0e2f730 | |||
| 492a42883e | |||
| b182f73415 | |||
| e766b9737e | |||
| 2335f93579 | |||
| 1730260343 | |||
| 27506e9ed8 | |||
| dc64a186d5 | |||
| 3163e09b98 | |||
| dcb9978bb1 | |||
| 4a94a0a5c5 | |||
| 8a2d20403e | |||
| ec706e95cc | |||
| bd3b14a27f | |||
| 082d9e852c | |||
| 36da519b26 | |||
| 06ffdde892 | |||
| 1ec57c080c | |||
| a635f27c68 | |||
| ee3d7a9a35 | |||
| 9a1c869efe | |||
| 837ed76f85 | |||
| b46589cd14 | |||
| d04e4606d2 | |||
| 385bd0eb8a | |||
| 089656e5c4 | |||
| 84ec6dd458 | |||
| 322c139c26 | |||
| babe1833bb | |||
| 9effa47dd8 | |||
| 7ef57cc0cf | |||
| 97420aae1b | |||
| 415e6309f9 | |||
| 83e63ff854 | |||
| de7f103130 | |||
| 2cb912681d | |||
| 04bdf94b78 | |||
| c7389ddaa7 | |||
| e778ab2e3a | |||
| 533d86607f | |||
| cb2096670f | |||
| 284f221a9d | |||
| bc639dd438 | |||
| 1baddbb40e | |||
| f784dab868 | |||
| 85192aaa21 | |||
| 054c705fe2 | |||
| 07b0d8cf6e | |||
| 597d16f566 | |||
| 0ca2c781c3 | |||
| f642de9c41 | |||
| 8965388d05 | |||
| 58c4582f15 | |||
| 44bc1b5cc0 | |||
| 714ebb3e08 | |||
| 8f871c2e3a | |||
| 5cdc5bc441 | |||
| 8d060837ad | |||
| 1d230d4cd6 | |||
| 3636ae7667 | |||
| 9ffb5112c6 | |||
| ca5d574cd7 | |||
| c80283dbcc | |||
| 3fcaddf2d3 | |||
| 6ecff5bce9 | |||
| a103c7dcb6 | |||
| 63746bbb47 | |||
| ed0be6fc9a | |||
| 26404ff5d7 | |||
| adf1674877 | |||
| ab2235fc88 | |||
| 441a6d3fe7 | |||
| e00397620a | |||
| 38fa58c0a3 | |||
| b40fd7b243 | |||
| ae34877496 | |||
| 599cf1e5cb | |||
| 474963dcf1 | |||
| e22384b6b4 | |||
| fb00652396 | |||
| a5dbb5d91f | |||
| e75a03b6f8 | |||
| eb7fe7f3e0 | |||
| 3179808f17 | |||
| fde9f05bd0 | |||
| 8de4290c5b | |||
| 19c74c8872 | |||
| 50edb5d1f4 | |||
| c6ccfd7e75 | |||
| 3796ce69e4 | |||
| 9835e31b46 | |||
| a35040c909 | |||
| a4c94638ca | |||
| e70a8ae6a0 | |||
| 100359e38d | |||
| cd995aca56 | |||
| 3a4bae88ca | |||
| e60eae27fb | |||
| cd6c01e230 | |||
| 0af264429f | |||
| a6d3862350 | |||
| 3fca4850dd | |||
| ba7e41d9a6 | |||
| fe33ce3413 | |||
| 4e25e8aaa2 | |||
| 91be826c7d | |||
| fdfe0cddb8 | |||
| e8ef62116f | |||
| caf8bb39d8 | |||
| 222ba6ee53 | |||
| 8dcda73072 | |||
| 810365d334 | |||
| 4b31510589 | |||
| dfce9a34b8 | |||
| dc9370c32b | |||
| 8dbc721c08 | |||
| 6448b84430 | |||
| 93d6ce40c3 | |||
| ce5be2c1be | |||
| 20fe837022 | |||
| e3ce18fa3e | |||
| 864a1d5e93 | |||
| 9cf7eec247 | |||
| d9c15621f6 | |||
| fea14218a9 | |||
| dbbded5250 | |||
| d65cfc7981 | |||
| dc9124f291 | |||
| 4cd433b6bc | |||
| f9a9ee6b0c | |||
| 1741f7ed58 | |||
| d459c751be | |||
| 34ef8b52f6 | |||
| 5ae96905bb | |||
| b1fdbc0151 | |||
| a5ad27b5f2 | |||
| efcd5052a2 | |||
| f2b10c0ba8 | |||
| f182be2d79 | |||
| 41b10630bb | |||
| 45915bed90 | |||
| a2c2ab428a | |||
| a05f74d302 | |||
| 74e94f3a97 | |||
| 15ee8c6cac | |||
| 18957b1f41 | |||
| 29930cac41 | |||
| e3338dc3ff | |||
| 97b7b4a501 | |||
| b471a72856 | |||
| fed7d911a3 | |||
| ca442970a3 | |||
| 9dbb77c10a | |||
| 1116502bc0 | |||
| edaf17bdd4 | |||
| c61d731358 | |||
| a8415a3484 | |||
| cd2467085e | |||
| 64efb3d2a4 | |||
| e05f137bd8 | |||
| 0c73ddc08b | |||
| 19cc43c442 | |||
| 7108fc81a9 | |||
| 5943b9d7d6 | |||
| 0271e4c918 | |||
| 9dc33eff3a | |||
| 5aef1c8a68 | |||
| c608a05270 | |||
| e2cfd247c3 | |||
| 97eb9154b2 | |||
| d7ff635445 | |||
| aff57fb54e | |||
| e89285a219 | |||
| 706f43caa8 | |||
| dc4faf57cb | |||
| 7baf8052a2 | |||
| d3c59585fd | |||
| 859bb8dc79 | |||
| 58cd2e07ba | |||
| a5a6fb590a | |||
| 3619993e68 | |||
| 88e12c78fa | |||
| 5c285b4ac6 | |||
| c6b729c470 | |||
| 890014759e | |||
| 68c1c43381 | |||
| d0dfcaaad5 | |||
| 3cffaddc0a | |||
| bf4cac0c82 | |||
| f680749a00 | |||
| 13a67980d9 | |||
| f110d595d2 | |||
| 9c8857352b | |||
| c09a1fdba8 | |||
| cdc7033a51 | |||
| fa30c759d7 | |||
| d040be2df0 | |||
| 935c831a7f | |||
| 867e95eef1 | |||
| 2ee04bd1b6 | |||
| 75d567e555 | |||
| d8a489971c | |||
| 19ce5b5c76 | |||
| 7c70ea4d3e | |||
| 2784285d47 | |||
| c946a7a1d5 | |||
| 3e60b49b8b | |||
| 4e7331bbb8 | |||
| b8c7e86223 | |||
| 3b925f8674 | |||
| f1f6d41c73 | |||
| 29ef1cb1be | |||
| 4296085d65 | |||
| c797b09228 | |||
| a870ef0030 | |||
| 43ed9e7310 | |||
| bcd27355f9 | |||
| 6a14dc69c0 | |||
| ed9acd25f9 | |||
| 7b24e66ed3 | |||
| abd3d4b546 | |||
| 4040c4240a | |||
| 1ee747f3ef | |||
| f88874bec8 | |||
| ed440a2150 | |||
| 2fd46b196b | |||
| 12dfcaf7e7 | |||
| f4a199f621 | |||
| bb708e0aa3 | |||
| d625740ca4 | |||
| 250402e9b9 | |||
| 1d2ffe56fb | |||
| d16c0d2887 | |||
| b3555f2f94 | |||
| 83a638fc6d | |||
| f1534a710f | |||
| a16845340b | |||
| ffa4725f8e | |||
| 7792c66c64 | |||
| 1a3985d709 | |||
| 4714895c59 | |||
| 1e37951701 | |||
| e8be1ad752 | |||
| e316a70b6c | |||
| 40a8d21c15 | |||
| 28d5ca7ed9 | |||
| 110b18545f | |||
| a478605da4 | |||
| f5f1589813 | |||
| 0c332b6adb | |||
| ba712ce357 | |||
| 2d2395accf | |||
| 8634289b7a | |||
| 45043fb9a8 | |||
| 0449795725 | |||
| a96093f1b7 | |||
| bd4f7691e9 | |||
| e12acbae70 | |||
| 48dc4eac10 | |||
| a869c92eee | |||
| 4fefd14538 | |||
| c09dbfa47c | |||
| d3c9f66de6 | |||
| 01d7694108 | |||
| 1425b651d4 | |||
| b1befbeefc | |||
| 3a9a84a0b1 | |||
| 368284cccc | |||
| ef777f4db9 | |||
| a8e4e8e882 | |||
| cf93760d00 | |||
| dd8b9ff8fb | |||
| bfed03b7b5 | |||
| 860f06ec9e | |||
| b58376920f | |||
| 4ace075ddf | |||
| dda98a474d | |||
| f1c0df7d87 | |||
| c78e098cb4 | |||
| a3438c4f8d | |||
| 92ecf2d5de | |||
| f18b653725 | |||
| 5128438cfb | |||
| f29f25822b | |||
| ecfe218840 | |||
| dd33d2b5d0 | |||
| 12a8d4e10b | |||
| c5c2fb31b1 | |||
| 343b7faf98 | |||
| 18aa8bbf60 | |||
| a358d1630f | |||
| 01375b321c | |||
| d2739d52e0 | |||
| 4668510106 | |||
| ffcd311c90 | |||
| b94a636542 | |||
| a7aec6bfbc | |||
| 190ca9eddd | |||
| 2cf9eb69eb | |||
| ffcb90da52 | |||
| 878b0c9275 | |||
| 5505cb0dea | |||
| 7ac14dccda | |||
| 6cffd0a723 | |||
| 220ebf93c7 | |||
| d0681a5592 | |||
| 09d167c16d | |||
| 477bb45df7 | |||
| e006306036 | |||
| 065cbcf0f9 | |||
| 7a6b958bbe | |||
| ef6a5b6599 | |||
| cdae919b5e | |||
| 12889f4549 | |||
| 089d59b691 | |||
| b3e247e9cc | |||
| 56392b87f7 | |||
| 1b1a4aeb38 | |||
| 16147e0c08 | |||
| 139317cf1b | |||
| 72b94127fb | |||
| 1f1fc94d22 | |||
| a574fe026c | |||
| aa82083d30 | |||
| 08d5df70c2 | |||
| 29b8fa5897 | |||
| e96faf31d4 | |||
| 157a73aa99 | |||
| bdd298c8a0 | |||
| 3f7dd21186 | |||
| 086b708cf7 | |||
| 57e0e57f48 | |||
| 4b7efbfdc0 | |||
| 7dc2653042 | |||
| e428453835 | |||
| f84c8229de | |||
| a73427d68d | |||
| e4456bb236 | |||
| 06eadd0c15 | |||
| 3c90dfa660 | |||
| ace1b8ee71 | |||
| 676356e800 | |||
| f732e54c22 | |||
| cdc2e74f68 | |||
| 724f3e872b | |||
| d63e5165eb | |||
| 9892c4392e | |||
| 5ced1a775c | |||
| 761de1318e | |||
| 02508512d5 | |||
| 6e6105af05 | |||
| d569419e13 | |||
| 93f1641803 | |||
| ff52bf93fa | |||
| a039275a0c | |||
| a98d10104d | |||
| 8924bc59b1 | |||
| eefe60a9c9 | |||
| fe1cb3d904 | |||
| 0448278a78 | |||
| 99c0c2ff4c | |||
| b369b734ca | |||
| 57150a20fd | |||
| 1634d7d531 | |||
| d563de4207 |
@@ -6,6 +6,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '4.**'
|
- '4.**'
|
||||||
|
- '5.**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -21,7 +22,7 @@ jobs:
|
|||||||
java-version: 1.8
|
java-version: 1.8
|
||||||
|
|
||||||
- name: Install NDK
|
- 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}
|
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;21.0.6113669" --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
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Reproducible Build Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 5 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build image
|
||||||
|
run: cd reproducible-builds && docker build -t signal-android . && cd ..
|
||||||
|
|
||||||
|
- name: Test build
|
||||||
|
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assembleRelease
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
.classpath
|
.classpath
|
||||||
captures/
|
captures/
|
||||||
project.properties
|
project.properties
|
||||||
|
keystore.debug.properties
|
||||||
|
keystore.staging.properties
|
||||||
.project
|
.project
|
||||||
.settings
|
.settings
|
||||||
bin/
|
bin/
|
||||||
@@ -23,5 +25,5 @@ ffpr
|
|||||||
test/androidTestEspresso/res/values/arrays.xml
|
test/androidTestEspresso/res/values/arrays.xml
|
||||||
obj/
|
obj/
|
||||||
jni/libspeex/.deps/
|
jni/libspeex/.deps/
|
||||||
*.sh
|
|
||||||
pkcs11.password
|
pkcs11.password
|
||||||
|
dev.keystore
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ There are several other ways to get involved:
|
|||||||
* Try to reproduce issues and help with troubleshooting.
|
* Try to reproduce issues and help with troubleshooting.
|
||||||
* Discover solutions to open issues and post any relevant findings.
|
* Discover solutions to open issues and post any relevant findings.
|
||||||
* Test other people's pull requests.
|
* Test other people's pull requests.
|
||||||
* Contribute to Signal via the [Freedom of the Press Foundation's donation page](https://freedom.press/crowdfunding/signal/).
|
* [Donate to Signal.](https://signal.org/donate/)
|
||||||
* Share Signal with your friends and family.
|
* Share Signal with your friends and family.
|
||||||
|
|
||||||
Signal is made for you. Thank you for your feedback and support.
|
Signal is made for you. Thank you for your feedback and support.
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
FROM ubuntu:17.10
|
|
||||||
|
|
||||||
RUN dpkg --add-architecture i386 && \
|
|
||||||
apt-get update -y && \
|
|
||||||
apt-get install -y software-properties-common && \
|
|
||||||
apt-get update -y && \
|
|
||||||
apt-get install -y libc6:i386=2.26-0ubuntu2.1 libncurses5:i386=6.0+20160625-1ubuntu1 libstdc++6:i386=7.2.0-8ubuntu3.2 lib32z1=1:1.2.11.dfsg-0ubuntu2 wget openjdk-8-jdk=8u171-b11-0ubuntu0.17.10.1 git unzip opensc pcscd && \
|
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
|
||||||
apt-get autoremove -y && \
|
|
||||||
apt-get clean
|
|
||||||
|
|
||||||
ENV ANDROID_SDK_FILENAME android-sdk_r24.4.1-linux.tgz
|
|
||||||
ENV ANDROID_SDK_URL https://dl.google.com/android/${ANDROID_SDK_FILENAME}
|
|
||||||
ENV ANDROID_API_LEVELS android-28
|
|
||||||
ENV ANDROID_BUILD_TOOLS_VERSION 28.0.3
|
|
||||||
ENV ANDROID_HOME /usr/local/android-sdk-linux
|
|
||||||
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
|
|
||||||
RUN cd /usr/local/ && \
|
|
||||||
wget -q ${ANDROID_SDK_URL} && \
|
|
||||||
tar -xzf ${ANDROID_SDK_FILENAME} && \
|
|
||||||
rm ${ANDROID_SDK_FILENAME}
|
|
||||||
RUN echo y | android update sdk --no-ui -a --filter ${ANDROID_API_LEVELS}
|
|
||||||
RUN echo y | android update sdk --no-ui -a --filter extra-android-m2repository,extra-android-support,extra-google-google_play_services,extra-google-m2repository
|
|
||||||
RUN echo y | android update sdk --no-ui -a --filter tools,platform-tools,build-tools-${ANDROID_BUILD_TOOLS_VERSION}
|
|
||||||
RUN rm -rf ${ANDROID_HOME}/tools && unzip ${ANDROID_HOME}/temp/*.zip -d ${ANDROID_HOME}
|
|
||||||
@@ -11,13 +11,15 @@ buildscript {
|
|||||||
jcenter {
|
jcenter {
|
||||||
content {
|
content {
|
||||||
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
|
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
|
||||||
|
includeGroupByRegex "com\\.archinamon.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
classpath 'com.android.tools.build:gradle:4.0.2'
|
||||||
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'
|
||||||
|
classpath 'com.archinamon:android-gradle-aspectj:4.2.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,9 +27,22 @@ apply plugin: 'com.android.application'
|
|||||||
apply plugin: 'com.google.protobuf'
|
apply plugin: 'com.google.protobuf'
|
||||||
apply plugin: 'androidx.navigation.safeargs'
|
apply plugin: 'androidx.navigation.safeargs'
|
||||||
apply plugin: 'witness'
|
apply plugin: 'witness'
|
||||||
|
apply plugin: 'com.archinamon.aspectj-ext'
|
||||||
apply from: 'translations.gradle'
|
apply from: 'translations.gradle'
|
||||||
apply from: 'witness-verifications.gradle'
|
apply from: 'witness-verifications.gradle'
|
||||||
|
|
||||||
|
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Internal")) {
|
||||||
|
aspectj {
|
||||||
|
includeJar 'sqlcipher'
|
||||||
|
includeAspectsFromJar 'Signal-Android'
|
||||||
|
java = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
} else if (getGradle().getStartParameter().getTaskRequests().toString().contains("Test")) {
|
||||||
|
aspectj {
|
||||||
|
compileTests = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
||||||
@@ -80,8 +95,8 @@ protobuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 686
|
def canonicalVersionCode = 745
|
||||||
def canonicalVersionName = "4.68.5"
|
def canonicalVersionName = "4.78.4"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['universal' : 0,
|
def abiPostFix = ['universal' : 0,
|
||||||
@@ -90,22 +105,35 @@ def abiPostFix = ['universal' : 0,
|
|||||||
'x86' : 3,
|
'x86' : 3,
|
||||||
'x86_64' : 4]
|
'x86_64' : 4]
|
||||||
|
|
||||||
|
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||||
|
|
||||||
android {
|
android {
|
||||||
flavorDimensions "none"
|
flavorDimensions 'distribution', 'environment'
|
||||||
compileSdkVersion 28
|
compileSdkVersion 30
|
||||||
buildToolsVersion '28.0.3'
|
buildToolsVersion '30.0.2'
|
||||||
useLibrary 'org.apache.http.legacy'
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
dexOptions {
|
dexOptions {
|
||||||
javaMaxHeapSize "4g"
|
javaMaxHeapSize "4g"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
if (keystores.debug != null) {
|
||||||
|
debug {
|
||||||
|
storeFile file("${project.rootDir}/${keystores.debug.storeFile}")
|
||||||
|
storePassword keystores.debug.storePassword
|
||||||
|
keyAlias keystores.debug.keyAlias
|
||||||
|
keyPassword keystores.debug.keyPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionCode canonicalVersionCode * postFixSize
|
versionCode canonicalVersionCode * postFixSize
|
||||||
versionName canonicalVersionName
|
versionName canonicalVersionName
|
||||||
|
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 28
|
targetSdkVersion 30
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@@ -119,16 +147,20 @@ android {
|
|||||||
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", "SIGNAL_SFU_URL", "\"https://sfu.voip.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", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
|
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||||
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||||
|
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
||||||
|
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
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"
|
||||||
|
buildConfigField "int", "TRACE_EVENT_MAX", "2000"
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
@@ -163,8 +195,16 @@ android {
|
|||||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aaptOptions {
|
||||||
|
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
|
if (keystores['debug'] != null) {
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
isDefault true
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||||
'proguard/proguard-firebase-messaging.pro',
|
'proguard/proguard-firebase-messaging.pro',
|
||||||
@@ -188,22 +228,9 @@ android {
|
|||||||
testProguardFiles 'proguard/proguard-automation.pro',
|
testProguardFiles 'proguard/proguard-automation.pro',
|
||||||
'proguard/proguard.cfg'
|
'proguard/proguard.cfg'
|
||||||
}
|
}
|
||||||
staging {
|
|
||||||
initWith debug
|
|
||||||
|
|
||||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
|
||||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
|
||||||
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
|
|
||||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
|
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
|
||||||
}
|
|
||||||
flipper {
|
flipper {
|
||||||
initWith debug
|
initWith debug
|
||||||
|
isDefault false
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
@@ -214,18 +241,53 @@ android {
|
|||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
play {
|
play {
|
||||||
dimension "none"
|
dimension 'distribution'
|
||||||
|
isDefault true
|
||||||
ext.websiteUpdateUrl = "null"
|
ext.websiteUpdateUrl = "null"
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||||
}
|
}
|
||||||
|
|
||||||
website {
|
website {
|
||||||
dimension "none"
|
dimension 'distribution'
|
||||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal {
|
||||||
|
dimension 'distribution'
|
||||||
|
ext.websiteUpdateUrl = "null"
|
||||||
|
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||||
|
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||||
|
buildConfigField "int", "TRACE_EVENT_MAX", "30_000"
|
||||||
|
}
|
||||||
|
|
||||||
|
prod {
|
||||||
|
dimension 'environment'
|
||||||
|
|
||||||
|
isDefault true
|
||||||
|
}
|
||||||
|
|
||||||
|
staging {
|
||||||
|
dimension 'environment'
|
||||||
|
|
||||||
|
applicationIdSuffix ".staging"
|
||||||
|
|
||||||
|
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||||
|
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||||
|
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||||
|
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||||
|
"\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\", " +
|
||||||
|
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||||
|
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||||
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||||
|
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android.applicationVariants.all { variant ->
|
android.applicationVariants.all { variant ->
|
||||||
@@ -256,27 +318,28 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
lintChecks project(':lintchecks')
|
lintChecks project(':lintchecks')
|
||||||
|
|
||||||
implementation('androidx.appcompat:appcompat:1.1.0-beta01') {
|
implementation ('androidx.appcompat:appcompat:1.2.0') {
|
||||||
force = true
|
force = true
|
||||||
}
|
}
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.1.0'
|
implementation 'com.google.android.material:material:1.2.1'
|
||||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.preference:preference:1.0.0'
|
implementation 'androidx.preference:preference:1.0.0'
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||||
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions: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-viewmodel-savedstate:1.0.0-alpha05'
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||||
implementation "androidx.camera:camera-core:1.0.0-beta01"
|
implementation "androidx.camera:camera-core:1.0.0-beta11"
|
||||||
implementation "androidx.camera:camera-camera2:1.0.0-beta01"
|
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
|
||||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta01"
|
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
|
||||||
|
implementation "androidx.camera:camera-view:1.0.0-alpha18"
|
||||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||||
implementation "androidx.autofill:autofill:1.0.0"
|
implementation "androidx.autofill:autofill:1.0.0"
|
||||||
implementation "androidx.paging:paging-common:2.1.2"
|
implementation "androidx.paging:paging-common:2.1.2"
|
||||||
@@ -295,6 +358,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||||
|
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
|
||||||
|
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||||
@@ -304,7 +368,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'org.signal:argon2:13.1@aar'
|
implementation 'org.signal:argon2:13.1@aar'
|
||||||
|
|
||||||
implementation 'org.signal:ringrtc-android:2.4.1'
|
implementation 'org.signal:ringrtc-android:2.8.3'
|
||||||
|
|
||||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||||
@@ -358,27 +422,27 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||||
testImplementation 'org.mockito:mockito-core:1.9.5'
|
testImplementation 'org.mockito:mockito-core:2.8.9'
|
||||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.5'
|
testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
|
||||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.5'
|
testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
|
||||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.5'
|
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4'
|
||||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.5'
|
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
|
||||||
|
|
||||||
testImplementation 'androidx.test:core:1.2.0'
|
testImplementation 'androidx.test:core:1.2.0'
|
||||||
testImplementation ('org.robolectric:robolectric:4.2') {
|
testImplementation ('org.robolectric:robolectric:4.4') {
|
||||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||||
}
|
}
|
||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
|
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyVerification {
|
dependencyVerification {
|
||||||
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
configuration = '(play|website)(Prod|Staging)(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");
|
||||||
@@ -422,28 +486,24 @@ def signProductionRelease = { variant ->
|
|||||||
|
|
||||||
task signProductionPlayRelease {
|
task signProductionPlayRelease {
|
||||||
doLast {
|
doLast {
|
||||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
|
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task signProductionInternalRelease {
|
||||||
|
doLast {
|
||||||
|
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task signProductionWebsiteRelease {
|
task signProductionWebsiteRelease {
|
||||||
doLast {
|
doLast {
|
||||||
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
|
def variant = android.applicationVariants.find { (it.name == 'websiteProdRelease') }
|
||||||
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
||||||
assembleWebsiteDescriptor(variant, signedRelease)
|
assembleWebsiteDescriptor(variant, signedRelease)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.whenTaskAdded { task ->
|
|
||||||
if (task.name.equals("assemblePlayRelease")) {
|
|
||||||
task.finalizedBy signProductionPlayRelease
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.name.equals("assembleWebsiteRelease")) {
|
|
||||||
task.finalizedBy signProductionWebsiteRelease
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getLastCommitTimestamp() {
|
def getLastCommitTimestamp() {
|
||||||
new ByteArrayOutputStream().withStream { os ->
|
new ByteArrayOutputStream().withStream { os ->
|
||||||
def result = exec {
|
def result = exec {
|
||||||
@@ -455,3 +515,24 @@ def getLastCommitTimestamp() {
|
|||||||
return os.toString() + "000"
|
return os.toString() + "000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(Test) {
|
||||||
|
testLogging {
|
||||||
|
events "failed"
|
||||||
|
exceptionFormat "full"
|
||||||
|
showCauses true
|
||||||
|
showExceptions true
|
||||||
|
showStackTraces true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def loadKeystoreProperties(filename) {
|
||||||
|
def keystorePropertiesFile = file("${project.rootDir}/${filename}")
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
return keystoreProperties;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,5 +5,12 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".FlipperApplicationContext"
|
android:name=".FlipperApplicationContext"
|
||||||
tools:replace="android:name"/>
|
tools:replace="android:name">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||||||
import net.sqlcipher.database.SQLiteStatement;
|
import net.sqlcipher.database.SQLiteStatement;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -29,13 +31,23 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
|
||||||
|
|
||||||
public FlipperSqlCipherAdapter(Context context) {
|
public FlipperSqlCipherAdapter(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Descriptor> getDatabases() {
|
public List<Descriptor> getDatabases() {
|
||||||
return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext())));
|
try {
|
||||||
|
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||||
|
databaseHelperField.setAccessible(true);
|
||||||
|
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
|
||||||
|
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.i(TAG, "Unable to use reflection to access raw database.", e);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">Signal (Flipper)</string>
|
|
||||||
</resources>
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package org.thoughtcrime.securesms.tracing;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses AspectJ to augment relevant methods to be traced with the {@link TracerImpl}.
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
public class TraceAspect {
|
||||||
|
|
||||||
|
@Pointcut("within(@org.thoughtcrime.securesms.tracing.Trace *)")
|
||||||
|
public void withinAnnotatedClass() {}
|
||||||
|
|
||||||
|
@Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
|
||||||
|
public void methodInsideAnnotatedType() {}
|
||||||
|
|
||||||
|
@Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
|
||||||
|
public void constructorInsideAnnotatedType() {}
|
||||||
|
|
||||||
|
@Pointcut("execution(@org.thoughtcrime.securesms.tracing.Trace * *(..)) || methodInsideAnnotatedType()")
|
||||||
|
public void annotatedMethod() {}
|
||||||
|
|
||||||
|
@Pointcut("execution(@org.thoughtcrime.securesms.tracing.Trace *.new(..)) || constructorInsideAnnotatedType()")
|
||||||
|
public void annotatedConstructor() {}
|
||||||
|
|
||||||
|
@Pointcut("execution(* *(..)) && within(net.sqlcipher.database.*)")
|
||||||
|
public void sqlcipher() {}
|
||||||
|
|
||||||
|
@Pointcut("execution(* net.sqlcipher.database.SQLiteDatabase.rawQuery(..)) || " +
|
||||||
|
"execution(* net.sqlcipher.database.SQLiteDatabase.query(..)) || " +
|
||||||
|
"execution(* net.sqlcipher.database.SQLiteDatabase.insert(..)) || " +
|
||||||
|
"execution(* net.sqlcipher.database.SQLiteDatabase.insertOrThrow(..)) || " +
|
||||||
|
"execution(* net.sqlcipher.database.SQLiteDatabase.insertWithOnConflict(..)) || " +
|
||||||
|
"execution(* net.sqlcipher.database.SQLiteDatabase.delete(..)) || " +
|
||||||
|
"execution(* net.sqlcipher.database.SQLiteDatabase.update(..))")
|
||||||
|
public void sqlcipherQuery() {}
|
||||||
|
|
||||||
|
@Around("annotatedMethod() || annotatedConstructor() || (sqlcipher() && !sqlcipherQuery())")
|
||||||
|
public @NonNull Object profile(@NonNull ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
String methodName = joinPoint.getSignature().toShortString();
|
||||||
|
|
||||||
|
Tracer.getInstance().start(methodName);
|
||||||
|
Object result = joinPoint.proceed();
|
||||||
|
Tracer.getInstance().end(methodName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Around("sqlcipherQuery()")
|
||||||
|
public @NonNull Object profileQuery(@NonNull ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
String table;
|
||||||
|
String query;
|
||||||
|
|
||||||
|
if (joinPoint.getSignature().getName().equals("query")) {
|
||||||
|
if (joinPoint.getArgs().length == 9) {
|
||||||
|
table = (String) joinPoint.getArgs()[1];
|
||||||
|
query = (String) joinPoint.getArgs()[3];
|
||||||
|
} else if (joinPoint.getArgs().length == 7 || joinPoint.getArgs().length == 8) {
|
||||||
|
table = (String) joinPoint.getArgs()[0];
|
||||||
|
query = (String) joinPoint.getArgs()[2];
|
||||||
|
} else {
|
||||||
|
table = "N/A";
|
||||||
|
query = "N/A";
|
||||||
|
}
|
||||||
|
} else if (joinPoint.getSignature().getName().equals("rawQuery")) {
|
||||||
|
table = "";
|
||||||
|
query = (String) joinPoint.getArgs()[0];
|
||||||
|
} else if (joinPoint.getSignature().getName().equals("insert")) {
|
||||||
|
table = (String) joinPoint.getArgs()[0];
|
||||||
|
query = "";
|
||||||
|
} else if (joinPoint.getSignature().getName().equals("insertOrThrow")) {
|
||||||
|
table = (String) joinPoint.getArgs()[0];
|
||||||
|
query = "";
|
||||||
|
} else if (joinPoint.getSignature().getName().equals("insertWithOnConflict")) {
|
||||||
|
table = (String) joinPoint.getArgs()[0];
|
||||||
|
query = "";
|
||||||
|
} else if (joinPoint.getSignature().getName().equals("delete")) {
|
||||||
|
table = (String) joinPoint.getArgs()[0];
|
||||||
|
query = (String) joinPoint.getArgs()[1];
|
||||||
|
} else if (joinPoint.getSignature().getName().equals("update")) {
|
||||||
|
table = (String) joinPoint.getArgs()[0];
|
||||||
|
query = (String) joinPoint.getArgs()[2];
|
||||||
|
} else {
|
||||||
|
table = "N/A";
|
||||||
|
query = "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query == null ? "null" : query;
|
||||||
|
query = "[" + table + "] " + query;
|
||||||
|
|
||||||
|
String methodName = joinPoint.getSignature().toShortString();
|
||||||
|
|
||||||
|
Tracer.getInstance().start(methodName, "query", query);
|
||||||
|
Object result = joinPoint.proceed();
|
||||||
|
Tracer.getInstance().end(methodName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package org.thoughtcrime.securesms.tracing;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.BuildConfig;
|
||||||
|
import org.thoughtcrime.securesms.trace.TraceProtos;
|
||||||
|
import org.thoughtcrime.securesms.trace.TraceProtos.Trace;
|
||||||
|
import org.thoughtcrime.securesms.trace.TraceProtos.TracePacket;
|
||||||
|
import org.thoughtcrime.securesms.trace.TraceProtos.TrackDescriptor;
|
||||||
|
import org.thoughtcrime.securesms.trace.TraceProtos.TrackEvent;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to create Perfetto-compatible traces. Currently keeps the entire trace in memory to
|
||||||
|
* avoid weirdness with synchronizing to disk.
|
||||||
|
*
|
||||||
|
* Some general info on how the Perfetto format works:
|
||||||
|
* - The file format is just a Trace proto (see Trace.proto)
|
||||||
|
* - The Trace proto is just a series of TracePackets
|
||||||
|
* - TracePackets can describe:
|
||||||
|
* - Threads
|
||||||
|
* - Start of a method
|
||||||
|
* - End of a method
|
||||||
|
* - (And a bunch of other stuff that's not relevant to use at this point)
|
||||||
|
*
|
||||||
|
* We keep a circular buffer of TracePackets for method calls, and we keep a separate list of
|
||||||
|
* TracePackets for threads so we don't lose any of those.
|
||||||
|
*
|
||||||
|
* Serializing is just a matter of throwing all the TracePackets we have into a proto.
|
||||||
|
*
|
||||||
|
* Note: This class aims to be largely-thread-safe, but prioritizes speed and memory efficiency
|
||||||
|
* above all else. These methods are going to be called very quickly from every thread imaginable,
|
||||||
|
* and we want to create as little overhead as possible. The idea being that it's ok if we don't,
|
||||||
|
* for example, keep a perfect circular buffer size if it allows us to reduce overhead. The only
|
||||||
|
* cost of screwing up would be dropping a trace packet or something, which, while sad, won't affect
|
||||||
|
* how the app functions.
|
||||||
|
*/
|
||||||
|
public final class TracerImpl implements Tracer {
|
||||||
|
|
||||||
|
private static final int TRUSTED_SEQUENCE_ID = 1;
|
||||||
|
private static final byte[] SYNCHRONIZATION_MARKER = UuidUtil.toByteArray(UUID.fromString("82477a76-b28d-42ba-81dc-33326d57a079"));
|
||||||
|
|
||||||
|
private final Clock clock;
|
||||||
|
private final Map<Long, TracePacket> threadPackets;
|
||||||
|
private final Queue<TracePacket> eventPackets;
|
||||||
|
private final AtomicInteger eventCount;
|
||||||
|
|
||||||
|
TracerImpl() {
|
||||||
|
this.clock = SystemClock::elapsedRealtimeNanos;
|
||||||
|
this.threadPackets = new ConcurrentHashMap<>();
|
||||||
|
this.eventPackets = new ConcurrentLinkedQueue<>();
|
||||||
|
this.eventCount = new AtomicInteger(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(@NonNull String methodName) {
|
||||||
|
long time = clock.getTimeNanos();
|
||||||
|
Thread currentThread = Thread.currentThread();
|
||||||
|
|
||||||
|
if (!threadPackets.containsKey(currentThread.getId())) {
|
||||||
|
threadPackets.put(currentThread.getId(), forThread(currentThread));
|
||||||
|
}
|
||||||
|
|
||||||
|
addPacket(forMethodStart(methodName, time, currentThread.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(@NonNull String methodName, @NonNull String key, @NonNull String value) {
|
||||||
|
long time = clock.getTimeNanos();
|
||||||
|
Thread currentThread = Thread.currentThread();
|
||||||
|
|
||||||
|
if (!threadPackets.containsKey(currentThread.getId())) {
|
||||||
|
threadPackets.put(currentThread.getId(), forThread(currentThread));
|
||||||
|
}
|
||||||
|
|
||||||
|
addPacket(forMethodStart(methodName, time, currentThread.getId(), key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void end(@NonNull String methodName) {
|
||||||
|
addPacket(forMethodEnd(methodName, clock.getTimeNanos(), Thread.currentThread().getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull byte[] serialize() {
|
||||||
|
Trace.Builder trace = Trace.newBuilder();
|
||||||
|
|
||||||
|
for (TracePacket thread : threadPackets.values()) {
|
||||||
|
trace.addPacket(thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TracePacket event : eventPackets) {
|
||||||
|
trace.addPacket(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.addPacket(forSynchronization(clock.getTimeNanos()));
|
||||||
|
|
||||||
|
return trace.build().toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to add a packet to our list while keeping the size of our circular buffer in-check.
|
||||||
|
* The tracking of the event count is not perfectly thread-safe, but doing it in a thread-safe
|
||||||
|
* way would likely involve adding a lock, which we really don't want to do, since it'll add
|
||||||
|
* unnecessary overhead.
|
||||||
|
*
|
||||||
|
* Note that we keep track of the event count separately because
|
||||||
|
* {@link ConcurrentLinkedQueue#size()} is NOT a constant-time operation.
|
||||||
|
*/
|
||||||
|
private void addPacket(@NonNull TracePacket packet) {
|
||||||
|
eventPackets.add(packet);
|
||||||
|
|
||||||
|
int size = eventCount.incrementAndGet();
|
||||||
|
|
||||||
|
for (int i = size; i > BuildConfig.TRACE_EVENT_MAX; i--) {
|
||||||
|
eventPackets.poll();
|
||||||
|
eventCount.decrementAndGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TracePacket forThread(@NonNull Thread thread) {
|
||||||
|
return TracePacket.newBuilder()
|
||||||
|
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||||
|
.setTrackDescriptor(TrackDescriptor.newBuilder()
|
||||||
|
.setUuid(thread.getId())
|
||||||
|
.setName(thread.getName()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TracePacket forMethodStart(@NonNull String name, long time, long threadId) {
|
||||||
|
return TracePacket.newBuilder()
|
||||||
|
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||||
|
.setTimestamp(time)
|
||||||
|
.setTrackEvent(TrackEvent.newBuilder()
|
||||||
|
.setTrackUuid(threadId)
|
||||||
|
.setName(name)
|
||||||
|
.setType(TrackEvent.Type.TYPE_SLICE_BEGIN))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TracePacket forMethodStart(@NonNull String name, long time, long threadId, @NonNull String key, @NonNull String value) {
|
||||||
|
return TracePacket.newBuilder()
|
||||||
|
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||||
|
.setTimestamp(time)
|
||||||
|
.setTrackEvent(TrackEvent.newBuilder()
|
||||||
|
.setTrackUuid(threadId)
|
||||||
|
.setName(name)
|
||||||
|
.setType(TrackEvent.Type.TYPE_SLICE_BEGIN)
|
||||||
|
.addDebugAnnotations(TraceProtos.DebugAnnotation.newBuilder()
|
||||||
|
.setName(key)
|
||||||
|
.setStringValue(value)
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TracePacket forMethodEnd(@NonNull String name, long time, long threadId) {
|
||||||
|
return TracePacket.newBuilder()
|
||||||
|
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||||
|
.setTimestamp(time)
|
||||||
|
.setTrackEvent(TrackEvent.newBuilder()
|
||||||
|
.setTrackUuid(threadId)
|
||||||
|
.setName(name)
|
||||||
|
.setType(TrackEvent.Type.TYPE_SLICE_END))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TracePacket forSynchronization(long time) {
|
||||||
|
return TracePacket.newBuilder()
|
||||||
|
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||||
|
.setTimestamp(time)
|
||||||
|
.setSynchronizationMarker(ByteString.copyFrom(SYNCHRONIZATION_MARKER))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface Clock {
|
||||||
|
long getTimeNanos();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package signal;
|
||||||
|
|
||||||
|
option java_package = "org.thoughtcrime.securesms.trace";
|
||||||
|
option java_outer_classname = "TraceProtos";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Minimal interface needed to work with Perfetto.
|
||||||
|
*
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/trace.proto
|
||||||
|
*/
|
||||||
|
message Trace {
|
||||||
|
repeated TracePacket packet = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TracePacket {
|
||||||
|
optional uint64 timestamp = 8;
|
||||||
|
optional uint32 timestamp_clock_id = 58;
|
||||||
|
|
||||||
|
oneof data {
|
||||||
|
TrackEvent track_event = 11;
|
||||||
|
TrackDescriptor track_descriptor = 60;
|
||||||
|
bytes synchronization_marker = 36;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof optional_trusted_packet_sequence_id {
|
||||||
|
uint32 trusted_packet_sequence_id = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message TrackEvent {
|
||||||
|
repeated uint64 category_iids = 3;
|
||||||
|
repeated string categories = 22;
|
||||||
|
|
||||||
|
repeated DebugAnnotation debug_annotations = 4;
|
||||||
|
|
||||||
|
oneof name_field {
|
||||||
|
uint64 name_iid = 10;
|
||||||
|
string name = 23;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
TYPE_UNSPECIFIED = 0;
|
||||||
|
TYPE_SLICE_BEGIN = 1;
|
||||||
|
TYPE_SLICE_END = 2;
|
||||||
|
TYPE_INSTANT = 3;
|
||||||
|
TYPE_COUNTER = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 9;
|
||||||
|
optional uint64 track_uuid = 11;
|
||||||
|
optional int64 counter_value = 30;
|
||||||
|
|
||||||
|
oneof timestamp {
|
||||||
|
int64 timestamp_delta_us = 1;
|
||||||
|
int64 timestamp_absolute_us = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof thread_time {
|
||||||
|
int64 thread_time_delta_us = 2;
|
||||||
|
int64 thread_time_absolute_us = 17;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message TrackDescriptor {
|
||||||
|
optional uint64 uuid = 1;
|
||||||
|
optional uint64 parent_uuid = 5;
|
||||||
|
optional string name = 2;
|
||||||
|
optional ThreadDescriptor thread = 4;
|
||||||
|
optional CounterDescriptor counter = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message ThreadDescriptor {
|
||||||
|
optional int32 pid = 1;
|
||||||
|
optional int32 tid = 2;
|
||||||
|
|
||||||
|
optional string thread_name = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CounterDescriptor {
|
||||||
|
enum BuiltinCounterType {
|
||||||
|
COUNTER_UNSPECIFIED = 0;
|
||||||
|
COUNTER_THREAD_TIME_NS = 1;
|
||||||
|
COUNTER_THREAD_INSTRUCTION_COUNT = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Unit {
|
||||||
|
UNIT_UNSPECIFIED = 0;
|
||||||
|
UNIT_TIME_NS = 1;
|
||||||
|
UNIT_COUNT = 2;
|
||||||
|
UNIT_SIZE_BYTES = 3;
|
||||||
|
}
|
||||||
|
optional BuiltinCounterType type = 1;
|
||||||
|
repeated string categories = 2;
|
||||||
|
optional Unit unit = 3;
|
||||||
|
optional int64 unit_multiplier = 4;
|
||||||
|
optional bool is_incremental = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DebugAnnotation {
|
||||||
|
message NestedValue {
|
||||||
|
enum NestedType {
|
||||||
|
UNSPECIFIED = 0;
|
||||||
|
DICT = 1;
|
||||||
|
ARRAY = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional NestedType nested_type = 1;
|
||||||
|
repeated string dict_keys = 2;
|
||||||
|
repeated NestedValue dict_values = 3;
|
||||||
|
repeated NestedValue array_values = 4;
|
||||||
|
optional int64 int_value = 5;
|
||||||
|
optional double double_value = 6;
|
||||||
|
optional bool bool_value = 7;
|
||||||
|
optional string string_value = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof name_field {
|
||||||
|
uint64 name_iid = 1;
|
||||||
|
string name = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof value {
|
||||||
|
bool bool_value = 2;
|
||||||
|
uint64 uint_value = 3;
|
||||||
|
int64 int_value = 4;
|
||||||
|
double double_value = 5;
|
||||||
|
string string_value = 6;
|
||||||
|
uint64 pointer_value = 7;
|
||||||
|
NestedValue nested_value = 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/core_red_shade"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
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,androidx.camera.lifecycle" />
|
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle,androidx.camera.view" />
|
||||||
|
|
||||||
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
<permission android:name="${applicationId}.ACCESS_SECRETS"
|
||||||
android:label="Access to TextSecure Secrets"
|
android:label="Access to TextSecure Secrets"
|
||||||
android:protectionLevel="signature" />
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
@@ -35,8 +35,10 @@
|
|||||||
<uses-permission android:name="android.permission.SEND_SMS"/>
|
<uses-permission android:name="android.permission.SEND_SMS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
@@ -113,7 +115,7 @@
|
|||||||
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
||||||
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
|
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
|
||||||
|
|
||||||
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity"
|
<activity android:name=".WebRtcCallActivity"
|
||||||
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
|
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
@@ -136,7 +138,7 @@
|
|||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
android:value=".MainActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".PromptMmsActivity"
|
<activity android:name=".PromptMmsActivity"
|
||||||
@@ -223,6 +225,23 @@
|
|||||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="sgnl"
|
||||||
|
android:host="signal.group" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="true"
|
||||||
|
tools:targetApi="23">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="https"
|
||||||
|
android:host="signal.group"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
||||||
android:resource="@mipmap/ic_launcher" />
|
android:resource="@mipmap/ic_launcher" />
|
||||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
||||||
@@ -260,6 +279,10 @@
|
|||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||||
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||||
|
|
||||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden"
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
@@ -421,7 +444,7 @@
|
|||||||
android:theme="@style/TextSecure.FullScreenMedia"
|
android:theme="@style/TextSecure.FullScreenMedia"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".BlockedContactsActivity"
|
<activity android:name=".blocked.BlockedUsersActivity"
|
||||||
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"/>
|
||||||
|
|
||||||
@@ -510,11 +533,28 @@
|
|||||||
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
|
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||||
|
|
||||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
<activity android:name=".megaphone.ClientDeprecatedActivity"
|
||||||
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||||
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
|
<service android:enabled="true" android:name=".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=".messages.IncomingMessageObserver$ForegroundService"/>
|
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||||
|
|
||||||
|
<service android:name=".components.voice.VoiceNotePlaybackService">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<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"
|
||||||
android:exported="true" >
|
android:exported="true" >
|
||||||
@@ -627,18 +667,25 @@
|
|||||||
|
|
||||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||||
|
|
||||||
|
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
|
||||||
|
|
||||||
<provider android:name=".providers.PartProvider"
|
<provider android:name=".providers.PartProvider"
|
||||||
android:grantUriPermissions="true"
|
android:grantUriPermissions="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:authorities="org.thoughtcrime.provider.securesms" />
|
android:authorities="${applicationId}.part" />
|
||||||
|
|
||||||
|
<provider android:name=".providers.BlobContentProvider"
|
||||||
|
android:authorities="${applicationId}.blob"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true" />
|
||||||
|
|
||||||
<provider android:name=".providers.MmsBodyProvider"
|
<provider android:name=".providers.MmsBodyProvider"
|
||||||
android:grantUriPermissions="true"
|
android:grantUriPermissions="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:authorities="org.thoughtcrime.provider.securesms.mms" />
|
android:authorities="${applicationId}.mms" />
|
||||||
|
|
||||||
<provider android:name="androidx.core.content.FileProvider"
|
<provider android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="org.thoughtcrime.securesms.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
|
|
||||||
@@ -647,23 +694,23 @@
|
|||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$Conversation"
|
<provider android:name=".database.DatabaseContentProviders$Conversation"
|
||||||
android:authorities="org.thoughtcrime.securesms.database.conversation"
|
android:authorities="${applicationId}.database.conversation"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$ConversationList"
|
<provider android:name=".database.DatabaseContentProviders$ConversationList"
|
||||||
android:authorities="org.thoughtcrime.securesms.database.conversationlist"
|
android:authorities="${applicationId}.database.conversationlist"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$Attachment"
|
<provider android:name=".database.DatabaseContentProviders$Attachment"
|
||||||
android:authorities="org.thoughtcrime.securesms.database.attachment"
|
android:authorities="${applicationId}.database.attachment"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$Sticker"
|
<provider android:name=".database.DatabaseContentProviders$Sticker"
|
||||||
android:authorities="org.thoughtcrime.securesms.database.sticker"
|
android:authorities="${applicationId}.database.sticker"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$StickerPack"
|
<provider android:name=".database.DatabaseContentProviders$StickerPack"
|
||||||
android:authorities="org.thoughtcrime.securesms.database.stickerpack"
|
android:authorities="${applicationId}.database.stickerpack"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name=".service.BootReceiver">
|
<receiver android:name=".service.BootReceiver">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 395 KiB After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 622 KiB After Width: | Height: | Size: 664 KiB |
|
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 559 KiB After Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 643 KiB After Width: | Height: | Size: 631 KiB |
|
Before Width: | Height: | Size: 647 KiB After Width: | Height: | Size: 653 KiB |
|
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 652 KiB |
|
Before Width: | Height: | Size: 589 KiB After Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 531 KiB After Width: | Height: | Size: 685 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 589 KiB After Width: | Height: | Size: 603 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 391 KiB |
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
package androidx.camera.view;
|
||||||
|
|
||||||
import android.Manifest.permission;
|
import android.Manifest.permission;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@@ -45,43 +45,44 @@ import androidx.annotation.RestrictTo;
|
|||||||
import androidx.annotation.RestrictTo.Scope;
|
import androidx.annotation.RestrictTo.Scope;
|
||||||
import androidx.camera.core.Camera;
|
import androidx.camera.core.Camera;
|
||||||
import androidx.camera.core.CameraSelector;
|
import androidx.camera.core.CameraSelector;
|
||||||
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
|
|
||||||
import androidx.camera.core.FocusMeteringAction;
|
import androidx.camera.core.FocusMeteringAction;
|
||||||
import androidx.camera.core.FocusMeteringResult;
|
import androidx.camera.core.FocusMeteringResult;
|
||||||
import androidx.camera.core.ImageCapture;
|
import androidx.camera.core.ImageCapture;
|
||||||
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
|
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
|
||||||
|
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
|
||||||
import androidx.camera.core.ImageProxy;
|
import androidx.camera.core.ImageProxy;
|
||||||
|
import androidx.camera.core.Logger;
|
||||||
import androidx.camera.core.MeteringPoint;
|
import androidx.camera.core.MeteringPoint;
|
||||||
|
import androidx.camera.core.MeteringPointFactory;
|
||||||
|
import androidx.camera.core.VideoCapture;
|
||||||
|
import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
|
||||||
import androidx.camera.core.impl.LensFacingConverter;
|
import androidx.camera.core.impl.LensFacingConverter;
|
||||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||||
import androidx.camera.core.impl.utils.futures.Futures;
|
import androidx.camera.core.impl.utils.futures.Futures;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import java.io.File;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link View} that displays a preview of the camera with methods {@link
|
* A {@link View} that displays a preview of the camera with methods {@link
|
||||||
* #takePicture(Executor, OnImageCapturedCallback)},
|
* #takePicture(Executor, OnImageCapturedCallback)},
|
||||||
* {@link #startRecording(FileDescriptor, Executor, VideoCapture.OnVideoSavedCallback)} and {@link #stopRecording()}.
|
* {@link #takePicture(ImageCapture.OutputFileOptions, Executor, OnImageSavedCallback)},
|
||||||
|
* {@link #startRecording(File , Executor , OnVideoSavedCallback callback)}
|
||||||
|
* and {@link #stopRecording()}.
|
||||||
*
|
*
|
||||||
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
|
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
|
||||||
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
|
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
|
||||||
* LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
|
* LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
|
||||||
*/
|
*/
|
||||||
// Begin Signal Custom Code Block
|
|
||||||
@RequiresApi(21)
|
@RequiresApi(21)
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
// End Signal Custom Code Block
|
public final class SignalCameraView extends FrameLayout {
|
||||||
public final class CameraXView extends FrameLayout {
|
static final String TAG = SignalCameraView.class.getSimpleName();
|
||||||
static final String TAG = CameraXView.class.getSimpleName();
|
|
||||||
static final boolean DEBUG = false;
|
|
||||||
|
|
||||||
static final int INDEFINITE_VIDEO_DURATION = -1;
|
static final int INDEFINITE_VIDEO_DURATION = -1;
|
||||||
static final int INDEFINITE_VIDEO_SIZE = -1;
|
static final int INDEFINITE_VIDEO_SIZE = -1;
|
||||||
@@ -107,7 +108,7 @@ public final class CameraXView extends FrameLayout {
|
|||||||
// For pinch-to-zoom
|
// For pinch-to-zoom
|
||||||
private PinchToZoomGestureDetector mPinchToZoomGestureDetector;
|
private PinchToZoomGestureDetector mPinchToZoomGestureDetector;
|
||||||
private boolean mIsPinchToZoomEnabled = true;
|
private boolean mIsPinchToZoomEnabled = true;
|
||||||
CameraXModule mCameraModule;
|
SignalCameraXModule mCameraModule;
|
||||||
private final DisplayManager.DisplayListener mDisplayListener =
|
private final DisplayManager.DisplayListener mDisplayListener =
|
||||||
new DisplayListener() {
|
new DisplayListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -124,26 +125,25 @@ public final class CameraXView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
private PreviewView mPreviewView;
|
private PreviewView mPreviewView;
|
||||||
private ScaleType mScaleType = ScaleType.CENTER_CROP;
|
|
||||||
// For accessibility event
|
// For accessibility event
|
||||||
private MotionEvent mUpEvent;
|
private MotionEvent mUpEvent;
|
||||||
|
|
||||||
public CameraXView(@NonNull Context context) {
|
public SignalCameraView(@NonNull Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||||
this(context, attrs, 0);
|
this(context, attrs, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
|
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
|
||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
init(context, attrs);
|
init(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(21)
|
@RequiresApi(21)
|
||||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
||||||
int defStyleRes) {
|
int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
init(context, attrs);
|
init(context, attrs);
|
||||||
}
|
}
|
||||||
@@ -172,23 +172,23 @@ public final class CameraXView extends FrameLayout {
|
|||||||
|
|
||||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||||
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
|
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
|
||||||
mCameraModule = new CameraXModule(this);
|
mCameraModule = new SignalCameraXModule(this);
|
||||||
|
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraXView);
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
|
||||||
setScaleType(
|
setScaleType(
|
||||||
ScaleType.fromId(
|
PreviewView.ScaleType.fromId(
|
||||||
a.getInteger(R.styleable.CameraXView_scaleType,
|
a.getInteger(R.styleable.CameraView_scaleType,
|
||||||
getScaleType().getId())));
|
getScaleType().getId())));
|
||||||
setPinchToZoomEnabled(
|
setPinchToZoomEnabled(
|
||||||
a.getBoolean(
|
a.getBoolean(
|
||||||
R.styleable.CameraXView_pinchToZoomEnabled, isPinchToZoomEnabled()));
|
R.styleable.CameraView_pinchToZoomEnabled, isPinchToZoomEnabled()));
|
||||||
setCaptureMode(
|
setCaptureMode(
|
||||||
CaptureMode.fromId(
|
CaptureMode.fromId(
|
||||||
a.getInteger(R.styleable.CameraXView_captureMode,
|
a.getInteger(R.styleable.CameraView_captureMode,
|
||||||
getCaptureMode().getId())));
|
getCaptureMode().getId())));
|
||||||
|
|
||||||
int lensFacing = a.getInt(R.styleable.CameraXView_lensFacing, LENS_FACING_BACK);
|
int lensFacing = a.getInt(R.styleable.CameraView_lensFacing, LENS_FACING_BACK);
|
||||||
switch (lensFacing) {
|
switch (lensFacing) {
|
||||||
case LENS_FACING_NONE:
|
case LENS_FACING_NONE:
|
||||||
setCameraLensFacing(null);
|
setCameraLensFacing(null);
|
||||||
@@ -203,7 +203,7 @@ public final class CameraXView extends FrameLayout {
|
|||||||
// Unhandled event.
|
// Unhandled event.
|
||||||
}
|
}
|
||||||
|
|
||||||
int flashMode = a.getInt(R.styleable.CameraXView_flash, 0);
|
int flashMode = a.getInt(R.styleable.CameraView_flash, 0);
|
||||||
switch (flashMode) {
|
switch (flashMode) {
|
||||||
case FLASH_MODE_AUTO:
|
case FLASH_MODE_AUTO:
|
||||||
setFlash(ImageCapture.FLASH_MODE_AUTO);
|
setFlash(ImageCapture.FLASH_MODE_AUTO);
|
||||||
@@ -265,7 +265,7 @@ public final class CameraXView extends FrameLayout {
|
|||||||
if (savedState instanceof Bundle) {
|
if (savedState instanceof Bundle) {
|
||||||
Bundle state = (Bundle) savedState;
|
Bundle state = (Bundle) savedState;
|
||||||
super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
|
super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
|
||||||
setScaleType(ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
|
setScaleType(PreviewView.ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
|
||||||
setZoomRatio(state.getFloat(EXTRA_ZOOM_RATIO));
|
setZoomRatio(state.getFloat(EXTRA_ZOOM_RATIO));
|
||||||
setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
|
setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
|
||||||
setFlash(FlashModeConverter.valueOf(state.getString(EXTRA_FLASH)));
|
setFlash(FlashModeConverter.valueOf(state.getString(EXTRA_FLASH)));
|
||||||
@@ -298,6 +298,21 @@ public final class CameraXView extends FrameLayout {
|
|||||||
dpyMgr.unregisterDisplayListener(mDisplayListener);
|
dpyMgr.unregisterDisplayListener(mDisplayListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the {@link LiveData} of the underlying {@link PreviewView}'s
|
||||||
|
* {@link PreviewView.StreamState}.
|
||||||
|
*
|
||||||
|
* @return A {@link LiveData} containing the {@link PreviewView.StreamState}. Apps can either
|
||||||
|
* get current value by {@link LiveData#getValue()} or register a observer by
|
||||||
|
* {@link LiveData#observe}.
|
||||||
|
* @see PreviewView#getPreviewStreamState()
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public LiveData<PreviewView.StreamState> getPreviewStreamState() {
|
||||||
|
return mPreviewView.getPreviewStreamState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
PreviewView getPreviewView() {
|
PreviewView getPreviewView() {
|
||||||
return mPreviewView;
|
return mPreviewView;
|
||||||
}
|
}
|
||||||
@@ -347,11 +362,11 @@ public final class CameraXView extends FrameLayout {
|
|||||||
/**
|
/**
|
||||||
* Returns the scale type used to scale the preview.
|
* Returns the scale type used to scale the preview.
|
||||||
*
|
*
|
||||||
* @return The current {@link ScaleType}.
|
* @return The current {@link PreviewView.ScaleType}.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public ScaleType getScaleType() {
|
public PreviewView.ScaleType getScaleType() {
|
||||||
return mScaleType;
|
return mPreviewView.getScaleType();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -359,13 +374,10 @@ public final class CameraXView extends FrameLayout {
|
|||||||
*
|
*
|
||||||
* <p>This controls how the view finder should be scaled and positioned within the view.
|
* <p>This controls how the view finder should be scaled and positioned within the view.
|
||||||
*
|
*
|
||||||
* @param scaleType The desired {@link ScaleType}.
|
* @param scaleType The desired {@link PreviewView.ScaleType}.
|
||||||
*/
|
*/
|
||||||
public void setScaleType(@NonNull ScaleType scaleType) {
|
public void setScaleType(@NonNull PreviewView.ScaleType scaleType) {
|
||||||
if (scaleType != mScaleType) {
|
mPreviewView.setScaleType(scaleType);
|
||||||
mScaleType = scaleType;
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -401,8 +413,10 @@ public final class CameraXView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the maximum video duration before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} is
|
* Sets the maximum video duration before
|
||||||
* called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
|
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)} is called
|
||||||
|
* automatically.
|
||||||
|
* Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
|
||||||
*/
|
*/
|
||||||
private void setMaxVideoDuration(long duration) {
|
private void setMaxVideoDuration(long duration) {
|
||||||
mCameraModule.setMaxVideoDuration(duration);
|
mCameraModule.setMaxVideoDuration(duration);
|
||||||
@@ -417,7 +431,8 @@ public final class CameraXView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the maximum video size in bytes before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)}
|
* Sets the maximum video size in bytes before
|
||||||
|
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)}
|
||||||
* is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
|
* is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
|
||||||
*/
|
*/
|
||||||
private void setMaxVideoSize(long size) {
|
private void setMaxVideoSize(long size) {
|
||||||
@@ -435,28 +450,38 @@ public final class CameraXView extends FrameLayout {
|
|||||||
mCameraModule.takePicture(executor, callback);
|
mCameraModule.takePicture(executor, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a picture and calls
|
||||||
|
* {@link OnImageSavedCallback#onImageSaved(ImageCapture.OutputFileResults)} when done.
|
||||||
|
*
|
||||||
|
* <p> The value of {@link ImageCapture.Metadata#isReversedHorizontal()} in the
|
||||||
|
* {@link ImageCapture.OutputFileOptions} will be overwritten based on camera direction. For
|
||||||
|
* front camera, it will be set to true; for back camera, it will be set to false.
|
||||||
|
*
|
||||||
|
* @param outputFileOptions Options to store the newly captured image.
|
||||||
|
* @param executor The executor in which the callback methods will be run.
|
||||||
|
* @param callback Callback which will receive success or failure.
|
||||||
|
*/
|
||||||
|
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
|
||||||
|
@NonNull Executor executor,
|
||||||
|
@NonNull OnImageSavedCallback callback) {
|
||||||
|
mCameraModule.takePicture(outputFileOptions, executor, callback);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a video and calls the OnVideoSavedCallback when done.
|
* Takes a video and calls the OnVideoSavedCallback when done.
|
||||||
*
|
*
|
||||||
* @param file The destination.
|
* @param outputFileOptions Options to store the newly captured video.
|
||||||
* @param executor The executor in which the callback methods will be run.
|
* @param executor The executor in which the callback methods will be run.
|
||||||
* @param callback Callback which will receive success or failure.
|
* @param callback Callback which will receive success or failure.
|
||||||
*/
|
*/
|
||||||
// Begin Signal Custom Code Block
|
public void startRecording(@NonNull VideoCapture.OutputFileOptions outputFileOptions,
|
||||||
@RequiresApi(26)
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
public void startRecording(// Begin Signal Custom Code Block
|
|
||||||
@NonNull FileDescriptor file,
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
@NonNull Executor executor,
|
@NonNull Executor executor,
|
||||||
@NonNull VideoCapture.OnVideoSavedCallback callback) {
|
@NonNull OnVideoSavedCallback callback) {
|
||||||
mCameraModule.startRecording(file, executor, callback);
|
mCameraModule.startRecording(outputFileOptions, executor, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stops an in progress video. */
|
/** Stops an in progress video. */
|
||||||
// Begin Signal Custom Code Block
|
|
||||||
@RequiresApi(26)
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
public void stopRecording() {
|
public void stopRecording() {
|
||||||
mCameraModule.stopRecording();
|
mCameraModule.stopRecording();
|
||||||
}
|
}
|
||||||
@@ -554,7 +579,8 @@ public final class CameraXView extends FrameLayout {
|
|||||||
mDownEventTimestamp = System.currentTimeMillis();
|
mDownEventTimestamp = System.currentTimeMillis();
|
||||||
break;
|
break;
|
||||||
case MotionEvent.ACTION_UP:
|
case MotionEvent.ACTION_UP:
|
||||||
if (delta() < ViewConfiguration.getLongPressTimeout()) {
|
if (delta() < ViewConfiguration.getLongPressTimeout()
|
||||||
|
&& mCameraModule.isBoundToLifecycle()) {
|
||||||
mUpEvent = event;
|
mUpEvent = event;
|
||||||
performClick();
|
performClick();
|
||||||
}
|
}
|
||||||
@@ -578,19 +604,14 @@ public final class CameraXView extends FrameLayout {
|
|||||||
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
|
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
|
||||||
mUpEvent = null;
|
mUpEvent = null;
|
||||||
|
|
||||||
CameraSelector cameraSelector =
|
|
||||||
new CameraSelector.Builder().requireLensFacing(
|
|
||||||
mCameraModule.getLensFacing()).build();
|
|
||||||
|
|
||||||
DisplayOrientedMeteringPointFactory pointFactory = new DisplayOrientedMeteringPointFactory(
|
|
||||||
getDisplay(), cameraSelector, mPreviewView.getWidth(), mPreviewView.getHeight());
|
|
||||||
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
|
|
||||||
float aePointWidth = afPointWidth * 1.5f;
|
|
||||||
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
|
|
||||||
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
|
|
||||||
|
|
||||||
Camera camera = mCameraModule.getCamera();
|
Camera camera = mCameraModule.getCamera();
|
||||||
if (camera != null) {
|
if (camera != null) {
|
||||||
|
MeteringPointFactory pointFactory = mPreviewView.getMeteringPointFactory();
|
||||||
|
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
|
||||||
|
float aePointWidth = afPointWidth * 1.5f;
|
||||||
|
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
|
||||||
|
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
|
||||||
|
|
||||||
ListenableFuture<FocusMeteringResult> future =
|
ListenableFuture<FocusMeteringResult> future =
|
||||||
camera.getCameraControl().startFocusAndMetering(
|
camera.getCameraControl().startFocusAndMetering(
|
||||||
new FocusMeteringAction.Builder(afPoint,
|
new FocusMeteringAction.Builder(afPoint,
|
||||||
@@ -609,7 +630,7 @@ public final class CameraXView extends FrameLayout {
|
|||||||
}, CameraXExecutors.directExecutor());
|
}, CameraXExecutors.directExecutor());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "cannot access camera");
|
Logger.d(TAG, "cannot access camera");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -711,45 +732,11 @@ public final class CameraXView extends FrameLayout {
|
|||||||
return mCameraModule.isTorchOn();
|
return mCameraModule.isTorchOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options for scaling the bounds of the view finder to the bounds of this view. */
|
|
||||||
public enum ScaleType {
|
|
||||||
/**
|
|
||||||
* Scale the view finder, maintaining the source aspect ratio, so the view finder fills the
|
|
||||||
* entire view. This will cause the view finder to crop the source image if the camera
|
|
||||||
* aspect ratio does not match the view aspect ratio.
|
|
||||||
*/
|
|
||||||
CENTER_CROP(0),
|
|
||||||
/**
|
|
||||||
* Scale the view finder, maintaining the source aspect ratio, so the view finder is
|
|
||||||
* entirely contained within the view.
|
|
||||||
*/
|
|
||||||
CENTER_INSIDE(1);
|
|
||||||
|
|
||||||
private final int mId;
|
|
||||||
|
|
||||||
int getId() {
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScaleType(int id) {
|
|
||||||
mId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ScaleType fromId(int id) {
|
|
||||||
for (ScaleType st : values()) {
|
|
||||||
if (st.mId == id) {
|
|
||||||
return st;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The capture mode used by CameraView.
|
* The capture mode used by CameraView.
|
||||||
*
|
*
|
||||||
* <p>This enum can be used to determine which capture mode will be enabled for {@link
|
* <p>This enum can be used to determine which capture mode will be enabled for {@link
|
||||||
* CameraXView}.
|
* SignalCameraView}.
|
||||||
*/
|
*/
|
||||||
public enum CaptureMode {
|
public enum CaptureMode {
|
||||||
/** A mode where image capture is enabled. */
|
/** A mode where image capture is enabled. */
|
||||||
@@ -832,4 +819,4 @@ public final class CameraXView extends FrameLayout {
|
|||||||
public void onScaleEnd(ScaleGestureDetector detector) {
|
public void onScaleEnd(ScaleGestureDetector detector) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
package androidx.camera.view;
|
||||||
|
|
||||||
|
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
|
||||||
|
|
||||||
import android.Manifest.permission;
|
import android.Manifest.permission;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@@ -27,17 +29,21 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.annotation.RequiresPermission;
|
import androidx.annotation.RequiresPermission;
|
||||||
|
import androidx.camera.core.AspectRatio;
|
||||||
import androidx.camera.core.Camera;
|
import androidx.camera.core.Camera;
|
||||||
|
import androidx.camera.core.CameraInfoUnavailableException;
|
||||||
import androidx.camera.core.CameraSelector;
|
import androidx.camera.core.CameraSelector;
|
||||||
import androidx.camera.core.CameraX;
|
|
||||||
import androidx.camera.core.ImageCapture;
|
import androidx.camera.core.ImageCapture;
|
||||||
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
|
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
|
||||||
|
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
|
||||||
|
import androidx.camera.core.Logger;
|
||||||
import androidx.camera.core.Preview;
|
import androidx.camera.core.Preview;
|
||||||
import androidx.camera.core.TorchState;
|
import androidx.camera.core.TorchState;
|
||||||
import androidx.camera.core.UseCase;
|
import androidx.camera.core.UseCase;
|
||||||
|
import androidx.camera.core.VideoCapture;
|
||||||
|
import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
|
||||||
import androidx.camera.core.impl.CameraInternal;
|
import androidx.camera.core.impl.CameraInternal;
|
||||||
import androidx.camera.core.impl.LensFacingConverter;
|
import androidx.camera.core.impl.LensFacingConverter;
|
||||||
import androidx.camera.core.impl.VideoCaptureConfig;
|
|
||||||
import androidx.camera.core.impl.utils.CameraOrientationUtil;
|
import androidx.camera.core.impl.utils.CameraOrientationUtil;
|
||||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||||
@@ -51,11 +57,10 @@ import androidx.lifecycle.OnLifecycleEvent;
|
|||||||
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -65,13 +70,10 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
|
|
||||||
|
|
||||||
/** CameraX use case operation built on @{link androidx.camera.core}. */
|
/** CameraX use case operation built on @{link androidx.camera.core}. */
|
||||||
// Begin Signal Custom Code Block
|
|
||||||
@RequiresApi(21)
|
@RequiresApi(21)
|
||||||
// End Signal Custom Code Block
|
@SuppressLint("RestrictedApi")
|
||||||
final class CameraXModule {
|
final class SignalCameraXModule {
|
||||||
public static final String TAG = "CameraXModule";
|
public static final String TAG = "CameraXModule";
|
||||||
|
|
||||||
private static final float UNITY_ZOOM_SCALE = 1f;
|
private static final float UNITY_ZOOM_SCALE = 1f;
|
||||||
@@ -82,13 +84,13 @@ final class CameraXModule {
|
|||||||
private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
|
private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
|
||||||
|
|
||||||
private final Preview.Builder mPreviewBuilder;
|
private final Preview.Builder mPreviewBuilder;
|
||||||
private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder;
|
private final VideoCapture.Builder mVideoCaptureBuilder;
|
||||||
private final ImageCapture.Builder mImageCaptureBuilder;
|
private final ImageCapture.Builder mImageCaptureBuilder;
|
||||||
private final CameraXView mCameraXView;
|
private final SignalCameraView mCameraView;
|
||||||
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
|
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
|
||||||
private CameraXView.CaptureMode mCaptureMode = CameraXView.CaptureMode.IMAGE;
|
private SignalCameraView.CaptureMode mCaptureMode = SignalCameraView.CaptureMode.IMAGE;
|
||||||
private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION;
|
private long mMaxVideoDuration = SignalCameraView.INDEFINITE_VIDEO_DURATION;
|
||||||
private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE;
|
private long mMaxVideoSize = SignalCameraView.INDEFINITE_VIDEO_SIZE;
|
||||||
@ImageCapture.FlashMode
|
@ImageCapture.FlashMode
|
||||||
private int mFlash = FLASH_MODE_OFF;
|
private int mFlash = FLASH_MODE_OFF;
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -110,7 +112,6 @@ final class CameraXModule {
|
|||||||
public void onDestroy(LifecycleOwner owner) {
|
public void onDestroy(LifecycleOwner owner) {
|
||||||
if (owner == mCurrentLifecycle) {
|
if (owner == mCurrentLifecycle) {
|
||||||
clearCurrentLifecycle();
|
clearCurrentLifecycle();
|
||||||
mPreview.setSurfaceProvider(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -123,8 +124,8 @@ final class CameraXModule {
|
|||||||
@Nullable
|
@Nullable
|
||||||
ProcessCameraProvider mCameraProvider;
|
ProcessCameraProvider mCameraProvider;
|
||||||
|
|
||||||
CameraXModule(CameraXView view) {
|
SignalCameraXModule(SignalCameraView view) {
|
||||||
mCameraXView = view;
|
mCameraView = view;
|
||||||
|
|
||||||
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
|
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
|
||||||
new FutureCallback<ProcessCameraProvider>() {
|
new FutureCallback<ProcessCameraProvider>() {
|
||||||
@@ -149,14 +150,12 @@ final class CameraXModule {
|
|||||||
|
|
||||||
mImageCaptureBuilder = new ImageCapture.Builder().setTargetName("ImageCapture");
|
mImageCaptureBuilder = new ImageCapture.Builder().setTargetName("ImageCapture");
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
mVideoCaptureBuilder = new VideoCapture.Builder().setTargetName("VideoCapture")
|
||||||
mVideoCaptureConfigBuilder =
|
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
|
||||||
new VideoCaptureConfig.Builder().setTargetName("VideoCapture")
|
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
|
||||||
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
|
.setBitRate(VideoUtil.VIDEO_BIT_RATE);
|
||||||
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
|
|
||||||
.setBitRate(VideoUtil.VIDEO_BIT_RATE);
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresPermission(permission.CAMERA)
|
@RequiresPermission(permission.CAMERA)
|
||||||
void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
||||||
mNewLifecycle = lifecycleOwner;
|
mNewLifecycle = lifecycleOwner;
|
||||||
@@ -173,12 +172,15 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearCurrentLifecycle();
|
clearCurrentLifecycle();
|
||||||
|
if (mNewLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
|
||||||
|
// Lifecycle is already in a destroyed state. Since it may have been a valid
|
||||||
|
// lifecycle when bound, but became destroyed while waiting for layout, treat this as
|
||||||
|
// a no-op now that we have cleared the previous lifecycle.
|
||||||
|
mNewLifecycle = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
mCurrentLifecycle = mNewLifecycle;
|
mCurrentLifecycle = mNewLifecycle;
|
||||||
mNewLifecycle = null;
|
mNewLifecycle = null;
|
||||||
if (mCurrentLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
||||||
mCurrentLifecycle = null;
|
|
||||||
throw new IllegalArgumentException("Cannot bind to lifecycle in a destroyed state.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mCameraProvider == null) {
|
if (mCameraProvider == null) {
|
||||||
// try again once the camera provider is no longer null
|
// try again once the camera provider is no longer null
|
||||||
@@ -188,18 +190,18 @@ final class CameraXModule {
|
|||||||
Set<Integer> available = getAvailableCameraLensFacing();
|
Set<Integer> available = getAvailableCameraLensFacing();
|
||||||
|
|
||||||
if (available.isEmpty()) {
|
if (available.isEmpty()) {
|
||||||
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
Logger.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
||||||
mCameraLensFacing = null;
|
mCameraLensFacing = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the current camera exists, or default to another camera
|
// Ensure the current camera exists, or default to another camera
|
||||||
if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
|
if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
|
||||||
Log.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
|
Logger.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
|
||||||
|
|
||||||
// Default to the first available camera direction
|
// Default to the first available camera direction
|
||||||
mCameraLensFacing = available.iterator().next();
|
mCameraLensFacing = available.iterator().next();
|
||||||
|
|
||||||
Log.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
|
Logger.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not attempt to create use cases for a null cameraLensFacing. This could occur if
|
// Do not attempt to create use cases for a null cameraLensFacing. This could occur if
|
||||||
@@ -216,14 +218,12 @@ final class CameraXModule {
|
|||||||
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|
||||||
|| getDisplayRotationDegrees() == 180;
|
|| getDisplayRotationDegrees() == 180;
|
||||||
|
|
||||||
Rational targetAspectRatio;
|
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
|
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
|
|
||||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
Rational targetAspectRatio;
|
||||||
// mImageCaptureBuilder.setTargetAspectRatio(AspectRatio.RATIO_4_3);
|
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
@@ -232,7 +232,6 @@ final class CameraXModule {
|
|||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
// mImageCaptureBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9);
|
|
||||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,15 +244,14 @@ final class CameraXModule {
|
|||||||
|
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
Size size = VideoUtil.getVideoRecordingSize();
|
Size size = VideoUtil.getVideoRecordingSize();
|
||||||
mVideoCaptureConfigBuilder.setTargetResolution(size);
|
mVideoCaptureBuilder.setTargetResolution(size);
|
||||||
mVideoCaptureConfigBuilder.setMaxResolution(size);
|
mVideoCaptureBuilder.setMaxResolution(size);
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
|
|
||||||
mVideoCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
mVideoCaptureBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
if (MediaConstraints.isVideoTranscodeAvailable()) {
|
if (MediaConstraints.isVideoTranscodeAvailable()) {
|
||||||
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.getUseCaseConfig());
|
mVideoCapture = mVideoCaptureBuilder.build();
|
||||||
}
|
}
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
|
|
||||||
@@ -262,15 +260,15 @@ final class CameraXModule {
|
|||||||
mPreviewBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
|
mPreviewBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
|
||||||
|
|
||||||
mPreview = mPreviewBuilder.build();
|
mPreview = mPreviewBuilder.build();
|
||||||
mPreview.setSurfaceProvider(mCameraXView.getPreviewView().getPreviewSurfaceProvider());
|
mPreview.setSurfaceProvider(mCameraView.getPreviewView().getSurfaceProvider());
|
||||||
|
|
||||||
CameraSelector cameraSelector =
|
CameraSelector cameraSelector =
|
||||||
new CameraSelector.Builder().requireLensFacing(mCameraLensFacing).build();
|
new CameraSelector.Builder().requireLensFacing(mCameraLensFacing).build();
|
||||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||||
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
||||||
mImageCapture,
|
mImageCapture,
|
||||||
mPreview);
|
mPreview);
|
||||||
} else if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
} else if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
|
||||||
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
||||||
mVideoCapture,
|
mVideoCapture,
|
||||||
mPreview);
|
mPreview);
|
||||||
@@ -301,7 +299,7 @@ final class CameraXModule {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
|
||||||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,17 +310,32 @@ final class CameraXModule {
|
|||||||
mImageCapture.takePicture(executor, callback);
|
mImageCapture.takePicture(executor, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
|
||||||
@RequiresApi(26)
|
@NonNull Executor executor, OnImageSavedCallback callback) {
|
||||||
public void startRecording(FileDescriptor file,
|
if (mImageCapture == null) {
|
||||||
// End Signal Custom Code Block
|
return;
|
||||||
Executor executor,
|
}
|
||||||
final VideoCapture.OnVideoSavedCallback callback) {
|
|
||||||
|
if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
|
||||||
|
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback == null) {
|
||||||
|
throw new IllegalArgumentException("OnImageSavedCallback should not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFileOptions.getMetadata().setReversedHorizontal(mCameraLensFacing != null
|
||||||
|
&& mCameraLensFacing == CameraSelector.LENS_FACING_FRONT);
|
||||||
|
mImageCapture.takePicture(outputFileOptions, executor, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startRecording(VideoCapture.OutputFileOptions outputFileOptions,
|
||||||
|
Executor executor, final OnVideoSavedCallback callback) {
|
||||||
if (mVideoCapture == null) {
|
if (mVideoCapture == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||||
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
|
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,15 +345,14 @@ final class CameraXModule {
|
|||||||
|
|
||||||
mVideoIsRecording.set(true);
|
mVideoIsRecording.set(true);
|
||||||
mVideoCapture.startRecording(
|
mVideoCapture.startRecording(
|
||||||
file,
|
outputFileOptions,
|
||||||
executor,
|
executor,
|
||||||
new VideoCapture.OnVideoSavedCallback() {
|
new VideoCapture.OnVideoSavedCallback() {
|
||||||
@Override
|
@Override
|
||||||
// Begin Signal Custom Code Block
|
public void onVideoSaved(
|
||||||
public void onVideoSaved(@NonNull FileDescriptor savedFile) {
|
@NonNull VideoCapture.OutputFileResults outputFileResults) {
|
||||||
// End Signal Custom Code Block
|
|
||||||
mVideoIsRecording.set(false);
|
mVideoIsRecording.set(false);
|
||||||
callback.onVideoSaved(savedFile);
|
callback.onVideoSaved(outputFileResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -349,15 +361,12 @@ final class CameraXModule {
|
|||||||
@NonNull String message,
|
@NonNull String message,
|
||||||
@Nullable Throwable cause) {
|
@Nullable Throwable cause) {
|
||||||
mVideoIsRecording.set(false);
|
mVideoIsRecording.set(false);
|
||||||
Log.e(TAG, message, cause);
|
Logger.e(TAG, message, cause);
|
||||||
callback.onError(videoCaptureError, message, cause);
|
callback.onError(videoCaptureError, message, cause);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
|
||||||
@RequiresApi(26)
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
public void stopRecording() {
|
public void stopRecording() {
|
||||||
if (mVideoCapture == null) {
|
if (mVideoCapture == null) {
|
||||||
return;
|
return;
|
||||||
@@ -388,14 +397,15 @@ final class CameraXModule {
|
|||||||
|
|
||||||
@RequiresPermission(permission.CAMERA)
|
@RequiresPermission(permission.CAMERA)
|
||||||
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
|
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
|
||||||
String cameraId;
|
if (mCameraProvider == null) {
|
||||||
try {
|
return false;
|
||||||
cameraId = CameraX.getCameraWithLensFacing(lensFacing);
|
}
|
||||||
} catch (Exception e) {
|
try {
|
||||||
throw new IllegalStateException("Unable to query lens facing.", e);
|
return mCameraProvider.hasCamera(
|
||||||
|
new CameraSelector.Builder().requireLensFacing(lensFacing).build());
|
||||||
|
} catch (CameraInfoUnavailableException e) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cameraId != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -454,7 +464,7 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
}, CameraXExecutors.directExecutor());
|
}, CameraXExecutors.directExecutor());
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Failed to set zoom ratio");
|
Logger.e(TAG, "Failed to set zoom ratio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +496,10 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isBoundToLifecycle() {
|
||||||
|
return mCamera != null;
|
||||||
|
}
|
||||||
|
|
||||||
int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
||||||
int rotationDegrees = 0;
|
int rotationDegrees = 0;
|
||||||
if (mCamera != null) {
|
if (mCamera != null) {
|
||||||
@@ -520,6 +534,11 @@ final class CameraXModule {
|
|||||||
if (!toUnbind.isEmpty()) {
|
if (!toUnbind.isEmpty()) {
|
||||||
mCameraProvider.unbind(toUnbind.toArray((new UseCase[0])));
|
mCameraProvider.unbind(toUnbind.toArray((new UseCase[0])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove surface provider once unbound.
|
||||||
|
if (mPreview != null) {
|
||||||
|
mPreview.setSurfaceProvider(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mCamera = null;
|
mCamera = null;
|
||||||
mCurrentLifecycle = null;
|
mCurrentLifecycle = null;
|
||||||
@@ -532,7 +551,7 @@ final class CameraXModule {
|
|||||||
mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
|
mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mVideoCapture != null && MediaConstraints.isVideoTranscodeAvailable()) {
|
if (mVideoCapture != null) {
|
||||||
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
|
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,7 +586,7 @@ final class CameraXModule {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CameraInternal camera = mImageCapture.getBoundCamera();
|
CameraInternal camera = mImageCapture.getCamera();
|
||||||
|
|
||||||
if (camera == null) {
|
if (camera == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -614,15 +633,15 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Context getContext() {
|
public Context getContext() {
|
||||||
return mCameraXView.getContext();
|
return mCameraView.getContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getWidth() {
|
public int getWidth() {
|
||||||
return mCameraXView.getWidth();
|
return mCameraView.getWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getHeight() {
|
public int getHeight() {
|
||||||
return mCameraXView.getHeight();
|
return mCameraView.getHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getDisplayRotationDegrees() {
|
public int getDisplayRotationDegrees() {
|
||||||
@@ -630,15 +649,15 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected int getDisplaySurfaceRotation() {
|
protected int getDisplaySurfaceRotation() {
|
||||||
return mCameraXView.getDisplaySurfaceRotation();
|
return mCameraView.getDisplaySurfaceRotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getMeasuredWidth() {
|
private int getMeasuredWidth() {
|
||||||
return mCameraXView.getMeasuredWidth();
|
return mCameraView.getMeasuredWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getMeasuredHeight() {
|
private int getMeasuredHeight() {
|
||||||
return mCameraXView.getMeasuredHeight();
|
return mCameraView.getMeasuredHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -647,11 +666,11 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public CameraXView.CaptureMode getCaptureMode() {
|
public SignalCameraView.CaptureMode getCaptureMode() {
|
||||||
return mCaptureMode;
|
return mCaptureMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCaptureMode(@NonNull CameraXView.CaptureMode captureMode) {
|
public void setCaptureMode(@NonNull SignalCameraView.CaptureMode captureMode) {
|
||||||
this.mCaptureMode = captureMode;
|
this.mCaptureMode = captureMode;
|
||||||
rebindToLifecycle();
|
rebindToLifecycle();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package org.signal.glide;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class Log {
|
||||||
|
|
||||||
|
private Log() {}
|
||||||
|
|
||||||
|
public static void v(@NonNull String tag, @NonNull String message) {
|
||||||
|
SignalGlideCodecs.getLogProvider().v(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void d(@NonNull String tag, @NonNull String message) {
|
||||||
|
SignalGlideCodecs.getLogProvider().d(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void i(@NonNull String tag, @NonNull String message) {
|
||||||
|
SignalGlideCodecs.getLogProvider().i(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void w(@NonNull String tag, @NonNull String message) {
|
||||||
|
SignalGlideCodecs.getLogProvider().w(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(@NonNull String tag, @NonNull String message) {
|
||||||
|
e(tag, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||||
|
SignalGlideCodecs.getLogProvider().e(tag, message, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Provider {
|
||||||
|
void v(@NonNull String tag, @NonNull String message);
|
||||||
|
void d(@NonNull String tag, @NonNull String message);
|
||||||
|
void i(@NonNull String tag, @NonNull String message);
|
||||||
|
void w(@NonNull String tag, @NonNull String message);
|
||||||
|
void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable);
|
||||||
|
|
||||||
|
Provider EMPTY = new Provider() {
|
||||||
|
@Override
|
||||||
|
public void v(@NonNull String tag, @NonNull String message) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void d(@NonNull String tag, @NonNull String message) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void i(@NonNull String tag, @NonNull String message) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void w(@NonNull String tag, @NonNull String message) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void e(@NonNull String tag, @NonNull String message, @NonNull Throwable throwable) { }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.signal.glide;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public final class SignalGlideCodecs {
|
||||||
|
|
||||||
|
private static Log.Provider logProvider = Log.Provider.EMPTY;
|
||||||
|
|
||||||
|
private SignalGlideCodecs() {}
|
||||||
|
|
||||||
|
public static void setLogProvider(@NonNull Log.Provider provider) {
|
||||||
|
logProvider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull Log.Provider getLogProvider() {
|
||||||
|
return logProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import org.signal.glide.common.FrameAnimationDrawable;
|
||||||
|
import org.signal.glide.apng.decode.APNGDecoder;
|
||||||
|
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||||
|
import org.signal.glide.common.loader.AssetStreamLoader;
|
||||||
|
import org.signal.glide.common.loader.FileLoader;
|
||||||
|
import org.signal.glide.common.loader.Loader;
|
||||||
|
import org.signal.glide.common.loader.ResourceStreamLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: APNGDrawable
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
public class APNGDrawable extends FrameAnimationDrawable<APNGDecoder> {
|
||||||
|
public APNGDrawable(Loader provider) {
|
||||||
|
super(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public APNGDrawable(APNGDecoder decoder) {
|
||||||
|
super(decoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected APNGDecoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener) {
|
||||||
|
return new APNGDecoder(streamLoader, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static APNGDrawable fromAsset(Context context, String assetPath) {
|
||||||
|
AssetStreamLoader assetStreamLoader = new AssetStreamLoader(context, assetPath);
|
||||||
|
return new APNGDrawable(assetStreamLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static APNGDrawable fromFile(String filePath) {
|
||||||
|
FileLoader fileLoader = new FileLoader(filePath);
|
||||||
|
return new APNGDrawable(fileLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static APNGDrawable fromResource(Context context, int resId) {
|
||||||
|
ResourceStreamLoader resourceStreamLoader = new ResourceStreamLoader(context, resId);
|
||||||
|
return new APNGDrawable(resourceStreamLoader);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27acTL.27:_The_Animation_Control_Chunk
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
class ACTLChunk extends Chunk {
|
||||||
|
static final int ID = fourCCToInt("acTL");
|
||||||
|
int num_frames;
|
||||||
|
int num_plays;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void innerParse(APNGReader apngReader) throws IOException {
|
||||||
|
num_frames = apngReader.readInt();
|
||||||
|
num_plays = apngReader.readInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
|
||||||
|
import org.signal.glide.Log;
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
import org.signal.glide.apng.io.APNGWriter;
|
||||||
|
import org.signal.glide.common.decode.Frame;
|
||||||
|
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
import org.signal.glide.common.loader.Loader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: APNG4Android
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-13
|
||||||
|
*/
|
||||||
|
public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||||
|
|
||||||
|
private static final String TAG = APNGDecoder.class.getSimpleName();
|
||||||
|
|
||||||
|
private APNGWriter apngWriter;
|
||||||
|
private int mLoopCount;
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
|
||||||
|
private class SnapShot {
|
||||||
|
byte dispose_op;
|
||||||
|
Rect dstRect = new Rect();
|
||||||
|
ByteBuffer byteBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SnapShot snapShot = new SnapShot();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param loader webp的reader
|
||||||
|
* @param renderListener 渲染的回调
|
||||||
|
*/
|
||||||
|
public APNGDecoder(Loader loader, FrameSeqDecoder.RenderListener renderListener) {
|
||||||
|
super(loader, renderListener);
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected APNGWriter getWriter() {
|
||||||
|
if (apngWriter == null) {
|
||||||
|
apngWriter = new APNGWriter();
|
||||||
|
}
|
||||||
|
return apngWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected APNGReader getReader(Reader reader) {
|
||||||
|
return new APNGReader(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLoopCount() {
|
||||||
|
return mLoopCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void release() {
|
||||||
|
snapShot.byteBuffer = null;
|
||||||
|
apngWriter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Rect read(APNGReader reader) throws IOException {
|
||||||
|
List<Chunk> chunks = APNGParser.parse(reader);
|
||||||
|
List<Chunk> otherChunks = new ArrayList<>();
|
||||||
|
|
||||||
|
boolean actl = false;
|
||||||
|
APNGFrame lastFrame = null;
|
||||||
|
byte[] ihdrData = new byte[0];
|
||||||
|
int canvasWidth = 0, canvasHeight = 0;
|
||||||
|
for (Chunk chunk : chunks) {
|
||||||
|
if (chunk instanceof ACTLChunk) {
|
||||||
|
mLoopCount = ((ACTLChunk) chunk).num_plays;
|
||||||
|
actl = true;
|
||||||
|
} else if (chunk instanceof FCTLChunk) {
|
||||||
|
APNGFrame frame = new APNGFrame(reader, (FCTLChunk) chunk);
|
||||||
|
frame.prefixChunks = otherChunks;
|
||||||
|
frame.ihdrData = ihdrData;
|
||||||
|
frames.add(frame);
|
||||||
|
lastFrame = frame;
|
||||||
|
} else if (chunk instanceof FDATChunk) {
|
||||||
|
if (lastFrame != null) {
|
||||||
|
lastFrame.imageChunks.add(chunk);
|
||||||
|
}
|
||||||
|
} else if (chunk instanceof IDATChunk) {
|
||||||
|
if (!actl) {
|
||||||
|
//如果为非APNG图片,则只解码PNG
|
||||||
|
Frame frame = new StillFrame(reader);
|
||||||
|
frame.frameWidth = canvasWidth;
|
||||||
|
frame.frameHeight = canvasHeight;
|
||||||
|
frames.add(frame);
|
||||||
|
mLoopCount = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (lastFrame != null) {
|
||||||
|
lastFrame.imageChunks.add(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (chunk instanceof IHDRChunk) {
|
||||||
|
canvasWidth = ((IHDRChunk) chunk).width;
|
||||||
|
canvasHeight = ((IHDRChunk) chunk).height;
|
||||||
|
ihdrData = ((IHDRChunk) chunk).data;
|
||||||
|
} else if (!(chunk instanceof IENDChunk)) {
|
||||||
|
otherChunks.add(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||||
|
snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||||
|
return new Rect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderFrame(Frame frame) {
|
||||||
|
if (frame == null || fullRect == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Bitmap bitmap = obtainBitmap(fullRect.width() / sampleSize, fullRect.height() / sampleSize);
|
||||||
|
Canvas canvas = cachedCanvas.get(bitmap);
|
||||||
|
if (canvas == null) {
|
||||||
|
canvas = new Canvas(bitmap);
|
||||||
|
cachedCanvas.put(bitmap, canvas);
|
||||||
|
}
|
||||||
|
if (frame instanceof APNGFrame) {
|
||||||
|
// 从缓存中恢复当前帧
|
||||||
|
frameBuffer.rewind();
|
||||||
|
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||||
|
// 开始绘制前,处理快照中的设定
|
||||||
|
if (this.frameIndex == 0) {
|
||||||
|
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||||
|
} else {
|
||||||
|
canvas.save();
|
||||||
|
canvas.clipRect(snapShot.dstRect);
|
||||||
|
switch (snapShot.dispose_op) {
|
||||||
|
// 从快照中恢复上一帧之前的显示内容
|
||||||
|
case FCTLChunk.APNG_DISPOSE_OP_PREVIOUS:
|
||||||
|
snapShot.byteBuffer.rewind();
|
||||||
|
bitmap.copyPixelsFromBuffer(snapShot.byteBuffer);
|
||||||
|
break;
|
||||||
|
// 清空上一帧所画区域
|
||||||
|
case FCTLChunk.APNG_DISPOSE_OP_BACKGROUND:
|
||||||
|
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||||
|
break;
|
||||||
|
// 什么都不做
|
||||||
|
case FCTLChunk.APNG_DISPOSE_OP_NON:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后根据dispose设定传递到快照信息中
|
||||||
|
if (((APNGFrame) frame).dispose_op == FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||||
|
if (snapShot.dispose_op != FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||||
|
snapShot.byteBuffer.rewind();
|
||||||
|
bitmap.copyPixelsToBuffer(snapShot.byteBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapShot.dispose_op = ((APNGFrame) frame).dispose_op;
|
||||||
|
canvas.save();
|
||||||
|
if (((APNGFrame) frame).blend_op == FCTLChunk.APNG_BLEND_OP_SOURCE) {
|
||||||
|
canvas.clipRect(
|
||||||
|
frame.frameX / sampleSize,
|
||||||
|
frame.frameY / sampleSize,
|
||||||
|
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||||
|
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||||
|
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
snapShot.dstRect.set(frame.frameX / sampleSize,
|
||||||
|
frame.frameY / sampleSize,
|
||||||
|
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||||
|
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
//开始真正绘制当前帧的内容
|
||||||
|
Bitmap inBitmap = obtainBitmap(frame.frameWidth, frame.frameHeight);
|
||||||
|
recycleBitmap(frame.draw(canvas, paint, sampleSize, inBitmap, getWriter()));
|
||||||
|
recycleBitmap(inBitmap);
|
||||||
|
frameBuffer.rewind();
|
||||||
|
bitmap.copyPixelsToBuffer(frameBuffer);
|
||||||
|
recycleBitmap(bitmap);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.e(TAG, "Failed to render!", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
import org.signal.glide.apng.io.APNGWriter;
|
||||||
|
import org.signal.glide.common.decode.Frame;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: APNG4Android
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-13
|
||||||
|
*/
|
||||||
|
public class APNGFrame extends Frame<APNGReader, APNGWriter> {
|
||||||
|
public final byte blend_op;
|
||||||
|
public final byte dispose_op;
|
||||||
|
byte[] ihdrData;
|
||||||
|
List<Chunk> imageChunks = new ArrayList<>();
|
||||||
|
List<Chunk> prefixChunks = new ArrayList<>();
|
||||||
|
private static final byte[] sPNGSignatures = {(byte) 137, 80, 78, 71, 13, 10, 26, 10};
|
||||||
|
private static final byte[] sPNGEndChunk = {0, 0, 0, 0, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82};
|
||||||
|
|
||||||
|
private static ThreadLocal<CRC32> sCRC32 = new ThreadLocal<>();
|
||||||
|
|
||||||
|
private CRC32 getCRC32() {
|
||||||
|
CRC32 crc32 = sCRC32.get();
|
||||||
|
if (crc32 == null) {
|
||||||
|
crc32 = new CRC32();
|
||||||
|
sCRC32.set(crc32);
|
||||||
|
}
|
||||||
|
return crc32;
|
||||||
|
}
|
||||||
|
|
||||||
|
public APNGFrame(APNGReader reader, FCTLChunk fctlChunk) {
|
||||||
|
super(reader);
|
||||||
|
blend_op = fctlChunk.blend_op;
|
||||||
|
dispose_op = fctlChunk.dispose_op;
|
||||||
|
frameDuration = fctlChunk.delay_num * 1000 / (fctlChunk.delay_den == 0 ? 100 : fctlChunk.delay_den);
|
||||||
|
frameWidth = fctlChunk.width;
|
||||||
|
frameHeight = fctlChunk.height;
|
||||||
|
frameX = fctlChunk.x_offset;
|
||||||
|
frameY = fctlChunk.y_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int encode(APNGWriter apngWriter) throws IOException {
|
||||||
|
int fileSize = 8 + 13 + 12;
|
||||||
|
|
||||||
|
//prefixChunks
|
||||||
|
for (Chunk chunk : prefixChunks) {
|
||||||
|
fileSize += chunk.length + 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
//imageChunks
|
||||||
|
for (Chunk chunk : imageChunks) {
|
||||||
|
if (chunk instanceof IDATChunk) {
|
||||||
|
fileSize += chunk.length + 12;
|
||||||
|
} else if (chunk instanceof FDATChunk) {
|
||||||
|
fileSize += chunk.length + 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileSize += sPNGEndChunk.length;
|
||||||
|
apngWriter.reset(fileSize);
|
||||||
|
apngWriter.putBytes(sPNGSignatures);
|
||||||
|
//IHDR Chunk
|
||||||
|
apngWriter.writeInt(13);
|
||||||
|
int start = apngWriter.position();
|
||||||
|
apngWriter.writeFourCC(IHDRChunk.ID);
|
||||||
|
apngWriter.writeInt(frameWidth);
|
||||||
|
apngWriter.writeInt(frameHeight);
|
||||||
|
apngWriter.putBytes(ihdrData);
|
||||||
|
CRC32 crc32 = getCRC32();
|
||||||
|
crc32.reset();
|
||||||
|
crc32.update(apngWriter.toByteArray(), start, 17);
|
||||||
|
apngWriter.writeInt((int) crc32.getValue());
|
||||||
|
|
||||||
|
//prefixChunks
|
||||||
|
for (Chunk chunk : prefixChunks) {
|
||||||
|
if (chunk instanceof IENDChunk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reader.reset();
|
||||||
|
reader.skip(chunk.offset);
|
||||||
|
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||||
|
apngWriter.skip(chunk.length + 12);
|
||||||
|
}
|
||||||
|
//imageChunks
|
||||||
|
for (Chunk chunk : imageChunks) {
|
||||||
|
if (chunk instanceof IDATChunk) {
|
||||||
|
reader.reset();
|
||||||
|
reader.skip(chunk.offset);
|
||||||
|
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||||
|
apngWriter.skip(chunk.length + 12);
|
||||||
|
} else if (chunk instanceof FDATChunk) {
|
||||||
|
apngWriter.writeInt(chunk.length - 4);
|
||||||
|
start = apngWriter.position();
|
||||||
|
apngWriter.writeFourCC(IDATChunk.ID);
|
||||||
|
|
||||||
|
reader.reset();
|
||||||
|
// skip to fdat data position
|
||||||
|
reader.skip(chunk.offset + 4 + 4 + 4);
|
||||||
|
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length - 4);
|
||||||
|
|
||||||
|
apngWriter.skip(chunk.length - 4);
|
||||||
|
crc32.reset();
|
||||||
|
crc32.update(apngWriter.toByteArray(), start, chunk.length);
|
||||||
|
apngWriter.writeInt((int) crc32.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endChunk
|
||||||
|
apngWriter.putBytes(sPNGEndChunk);
|
||||||
|
return fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||||
|
try {
|
||||||
|
int length = encode(writer);
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inJustDecodeBounds = false;
|
||||||
|
options.inSampleSize = sampleSize;
|
||||||
|
options.inMutable = true;
|
||||||
|
options.inBitmap = reusedBitmap;
|
||||||
|
byte[] bytes = writer.toByteArray();
|
||||||
|
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options);
|
||||||
|
assert bitmap != null;
|
||||||
|
canvas.drawBitmap(bitmap, (float) frameX / sampleSize, (float) frameY / sampleSize, paint);
|
||||||
|
return bitmap;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
import org.signal.glide.common.io.StreamReader;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link {https://www.w3.org/TR/PNG/#5PNG-file-signature}
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-13
|
||||||
|
*/
|
||||||
|
public class APNGParser {
|
||||||
|
static class FormatException extends IOException {
|
||||||
|
FormatException() {
|
||||||
|
super("APNG Format error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAPNG(String filePath) {
|
||||||
|
InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
inputStream = new FileInputStream(filePath);
|
||||||
|
return isAPNG(new StreamReader(inputStream));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (inputStream != null) {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAPNG(Context context, String assetPath) {
|
||||||
|
InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
inputStream = context.getAssets().open(assetPath);
|
||||||
|
return isAPNG(new StreamReader(inputStream));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (inputStream != null) {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAPNG(Context context, int resId) {
|
||||||
|
InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
inputStream = context.getResources().openRawResource(resId);
|
||||||
|
return isAPNG(new StreamReader(inputStream));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (inputStream != null) {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAPNG(Reader in) {
|
||||||
|
APNGReader reader = (in instanceof APNGReader) ? (APNGReader) in : new APNGReader(in);
|
||||||
|
try {
|
||||||
|
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
while (reader.available() > 0) {
|
||||||
|
Chunk chunk = parseChunk(reader);
|
||||||
|
if (chunk instanceof ACTLChunk) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Chunk> parse(APNGReader reader) throws IOException {
|
||||||
|
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Chunk> chunks = new ArrayList<>();
|
||||||
|
while (reader.available() > 0) {
|
||||||
|
chunks.add(parseChunk(reader));
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Chunk parseChunk(APNGReader reader) throws IOException {
|
||||||
|
int offset = reader.position();
|
||||||
|
int size = reader.readInt();
|
||||||
|
int fourCC = reader.readFourCC();
|
||||||
|
Chunk chunk;
|
||||||
|
if (fourCC == ACTLChunk.ID) {
|
||||||
|
chunk = new ACTLChunk();
|
||||||
|
} else if (fourCC == FCTLChunk.ID) {
|
||||||
|
chunk = new FCTLChunk();
|
||||||
|
} else if (fourCC == FDATChunk.ID) {
|
||||||
|
chunk = new FDATChunk();
|
||||||
|
} else if (fourCC == IDATChunk.ID) {
|
||||||
|
chunk = new IDATChunk();
|
||||||
|
} else if (fourCC == IENDChunk.ID) {
|
||||||
|
chunk = new IENDChunk();
|
||||||
|
} else if (fourCC == IHDRChunk.ID) {
|
||||||
|
chunk = new IHDRChunk();
|
||||||
|
} else {
|
||||||
|
chunk = new Chunk();
|
||||||
|
}
|
||||||
|
chunk.offset = offset;
|
||||||
|
chunk.fourcc = fourCC;
|
||||||
|
chunk.length = size;
|
||||||
|
chunk.parse(reader);
|
||||||
|
chunk.crc = reader.readInt();
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
|
||||||
|
* Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
|
||||||
|
* Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
|
||||||
|
* CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码
|
||||||
|
* @Link https://www.w3.org/TR/PNG
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
class Chunk {
|
||||||
|
int length;
|
||||||
|
int fourcc;
|
||||||
|
int crc;
|
||||||
|
int offset;
|
||||||
|
|
||||||
|
static int fourCCToInt(String fourCC) {
|
||||||
|
if (TextUtils.isEmpty(fourCC) || fourCC.length() != 4) {
|
||||||
|
return 0xbadeffff;
|
||||||
|
}
|
||||||
|
return (fourCC.charAt(0) & 0xff)
|
||||||
|
| (fourCC.charAt(1) & 0xff) << 8
|
||||||
|
| (fourCC.charAt(2) & 0xff) << 16
|
||||||
|
| (fourCC.charAt(3) & 0xff) << 24
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse(APNGReader reader) throws IOException {
|
||||||
|
int available = reader.available();
|
||||||
|
innerParse(reader);
|
||||||
|
int offset = available - reader.available();
|
||||||
|
if (offset > length) {
|
||||||
|
throw new IOException("Out of chunk area");
|
||||||
|
} else if (offset < length) {
|
||||||
|
reader.skip(length - offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void innerParse(APNGReader reader) throws IOException {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
* @see {link=https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fcTL.27:_The_Frame_Control_Chunk}
|
||||||
|
*/
|
||||||
|
class FCTLChunk extends Chunk {
|
||||||
|
static final int ID = fourCCToInt("fcTL");
|
||||||
|
int sequence_number;
|
||||||
|
/**
|
||||||
|
* x_offset >= 0
|
||||||
|
* y_offset >= 0
|
||||||
|
* width > 0
|
||||||
|
* height > 0
|
||||||
|
* x_offset + width <= 'IHDR' width
|
||||||
|
* y_offset + height <= 'IHDR' height
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Width of the following frame.
|
||||||
|
*/
|
||||||
|
int width;
|
||||||
|
/**
|
||||||
|
* Height of the following frame.
|
||||||
|
*/
|
||||||
|
int height;
|
||||||
|
/**
|
||||||
|
* X position at which to render the following frame.
|
||||||
|
*/
|
||||||
|
int x_offset;
|
||||||
|
/**
|
||||||
|
* Y position at which to render the following frame.
|
||||||
|
*/
|
||||||
|
int y_offset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The delay_num and delay_den parameters together specify a fraction indicating the time to
|
||||||
|
* display the current frame, in seconds. If the denominator is 0, it is to be treated as if it
|
||||||
|
* were 100 (that is, delay_num then specifies 1/100ths of a second).
|
||||||
|
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as
|
||||||
|
* possible, though viewers may impose a reasonable lower bound.
|
||||||
|
* <p>
|
||||||
|
* Frame timings should be independent of the time required for decoding and display of each frame,
|
||||||
|
* so that animations will run at the same rate regardless of the performance of the decoder implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frame delay fraction numerator.
|
||||||
|
*/
|
||||||
|
short delay_num;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frame delay fraction denominator.
|
||||||
|
*/
|
||||||
|
short delay_den;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of frame area disposal to be done after rendering this frame.
|
||||||
|
* dispose_op specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
|
||||||
|
* If the first 'fcTL' chunk uses a dispose_op of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
|
||||||
|
*/
|
||||||
|
byte dispose_op;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of frame area rendering for this frame.
|
||||||
|
*/
|
||||||
|
byte blend_op;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
|
||||||
|
*/
|
||||||
|
static final int APNG_DISPOSE_OP_NON = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
|
||||||
|
*/
|
||||||
|
static final int APNG_DISPOSE_OP_BACKGROUND = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
|
||||||
|
*/
|
||||||
|
static final int APNG_DISPOSE_OP_PREVIOUS = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* blend_op<code> specifies whether the frame is to be alpha blended into the current output buffer content,
|
||||||
|
* or whether it should completely replace its region in the output buffer.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
|
||||||
|
*/
|
||||||
|
static final int APNG_BLEND_OP_SOURCE = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame should be composited onto the output buffer based on its alpha,
|
||||||
|
* using a simple OVER operation as described in the Alpha Channel Processing section of the Extensions
|
||||||
|
* to the PNG Specification, Version 1.2.0. Note that the second variation of the sample code is applicable.
|
||||||
|
*/
|
||||||
|
static final int APNG_BLEND_OP_OVER = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void innerParse(APNGReader reader) throws IOException {
|
||||||
|
sequence_number = reader.readInt();
|
||||||
|
width = reader.readInt();
|
||||||
|
height = reader.readInt();
|
||||||
|
x_offset = reader.readInt();
|
||||||
|
y_offset = reader.readInt();
|
||||||
|
delay_num = reader.readShort();
|
||||||
|
delay_den = reader.readShort();
|
||||||
|
dispose_op = reader.peek();
|
||||||
|
blend_op = reader.peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fdAT.27:_The_Frame_Data_Chunk
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
class FDATChunk extends Chunk {
|
||||||
|
static final int ID = fourCCToInt("fdAT");
|
||||||
|
int sequence_number;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void innerParse(APNGReader reader) throws IOException {
|
||||||
|
sequence_number = reader.readInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 作用描述
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
class IDATChunk extends Chunk {
|
||||||
|
static final int ID = fourCCToInt("IDAT");
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 作用描述
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
class IENDChunk extends Chunk {
|
||||||
|
static final int ID = Chunk.fourCCToInt("IEND");
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IHDR chunk shall be the first chunk in the PNG datastream. It contains:
|
||||||
|
* <p>
|
||||||
|
* Width 4 bytes
|
||||||
|
* Height 4 bytes
|
||||||
|
* Bit depth 1 byte
|
||||||
|
* Colour type 1 byte
|
||||||
|
* Compression method 1 byte
|
||||||
|
* Filter method 1 byte
|
||||||
|
* Interlace method 1 byte
|
||||||
|
*
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
class IHDRChunk extends Chunk {
|
||||||
|
static final int ID = fourCCToInt("IHDR");
|
||||||
|
/**
|
||||||
|
* 图像宽度,以像素为单位
|
||||||
|
*/
|
||||||
|
int width;
|
||||||
|
/**
|
||||||
|
* 图像高度,以像素为单位
|
||||||
|
*/
|
||||||
|
int height;
|
||||||
|
|
||||||
|
byte[] data = new byte[5];
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void innerParse(APNGReader reader) throws IOException {
|
||||||
|
width = reader.readInt();
|
||||||
|
height = reader.readInt();
|
||||||
|
reader.read(data, 0, data.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.decode;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
|
||||||
|
import org.signal.glide.apng.io.APNGReader;
|
||||||
|
import org.signal.glide.apng.io.APNGWriter;
|
||||||
|
import org.signal.glide.common.decode.Frame;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: APNG4Android
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-13
|
||||||
|
*/
|
||||||
|
public class StillFrame extends Frame<APNGReader, APNGWriter> {
|
||||||
|
|
||||||
|
public StillFrame(APNGReader reader) {
|
||||||
|
super(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inJustDecodeBounds = false;
|
||||||
|
options.inSampleSize = sampleSize;
|
||||||
|
options.inMutable = true;
|
||||||
|
options.inBitmap = reusedBitmap;
|
||||||
|
Bitmap bitmap = null;
|
||||||
|
try {
|
||||||
|
reader.reset();
|
||||||
|
bitmap = BitmapFactory.decodeStream(reader.toInputStream(), null, options);
|
||||||
|
assert bitmap != null;
|
||||||
|
paint.setXfermode(null);
|
||||||
|
canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.io;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import org.signal.glide.common.io.FilterReader;
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: APNGReader
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-13
|
||||||
|
*/
|
||||||
|
public class APNGReader extends FilterReader {
|
||||||
|
private static ThreadLocal<byte[]> __intBytes = new ThreadLocal<>();
|
||||||
|
|
||||||
|
|
||||||
|
protected static byte[] ensureBytes() {
|
||||||
|
byte[] bytes = __intBytes.get();
|
||||||
|
if (bytes == null) {
|
||||||
|
bytes = new byte[4];
|
||||||
|
__intBytes.set(bytes);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public APNGReader(Reader in) {
|
||||||
|
super(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readInt() throws IOException {
|
||||||
|
byte[] buf = ensureBytes();
|
||||||
|
read(buf, 0, 4);
|
||||||
|
return buf[3] & 0xFF |
|
||||||
|
(buf[2] & 0xFF) << 8 |
|
||||||
|
(buf[1] & 0xFF) << 16 |
|
||||||
|
(buf[0] & 0xFF) << 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short readShort() throws IOException {
|
||||||
|
byte[] buf = ensureBytes();
|
||||||
|
read(buf, 0, 2);
|
||||||
|
return (short) (buf[1] & 0xFF |
|
||||||
|
(buf[0] & 0xFF) << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return read FourCC and match chars
|
||||||
|
*/
|
||||||
|
public boolean matchFourCC(String chars) throws IOException {
|
||||||
|
if (TextUtils.isEmpty(chars) || chars.length() != 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int fourCC = readFourCC();
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (((fourCC >> (i * 8)) & 0xff) != chars.charAt(i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readFourCC() throws IOException {
|
||||||
|
byte[] buf = ensureBytes();
|
||||||
|
read(buf, 0, 4);
|
||||||
|
return buf[0] & 0xff | (buf[1] & 0xff) << 8 | (buf[2] & 0xff) << 16 | (buf[3] & 0xff) << 24;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.apng.io;
|
||||||
|
|
||||||
|
import org.signal.glide.common.io.ByteBufferWriter;
|
||||||
|
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: APNGWriter
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-13
|
||||||
|
*/
|
||||||
|
public class APNGWriter extends ByteBufferWriter {
|
||||||
|
public APNGWriter() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeFourCC(int val) {
|
||||||
|
putByte((byte) (val & 0xff));
|
||||||
|
putByte((byte) ((val >> 8) & 0xff));
|
||||||
|
putByte((byte) ((val >> 16) & 0xff));
|
||||||
|
putByte((byte) ((val >> 24) & 0xff));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeInt(int val) {
|
||||||
|
putByte((byte) ((val >> 24) & 0xff));
|
||||||
|
putByte((byte) ((val >> 16) & 0xff));
|
||||||
|
putByte((byte) ((val >> 8) & 0xff));
|
||||||
|
putByte((byte) (val & 0xff));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(int size) {
|
||||||
|
super.reset(size);
|
||||||
|
this.byteBuffer.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.DrawFilter;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PaintFlagsDrawFilter;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||||
|
|
||||||
|
import org.signal.glide.Log;
|
||||||
|
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||||
|
import org.signal.glide.common.loader.Loader;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: Frame animation drawable
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
public abstract class FrameAnimationDrawable<Decoder extends FrameSeqDecoder> extends Drawable implements Animatable2Compat, FrameSeqDecoder.RenderListener {
|
||||||
|
private static final String TAG = FrameAnimationDrawable.class.getSimpleName();
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
private final Decoder frameSeqDecoder;
|
||||||
|
private DrawFilter drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||||
|
private Matrix matrix = new Matrix();
|
||||||
|
private Set<AnimationCallback> animationCallbacks = new HashSet<>();
|
||||||
|
private Bitmap bitmap;
|
||||||
|
private static final int MSG_ANIMATION_START = 1;
|
||||||
|
private static final int MSG_ANIMATION_END = 2;
|
||||||
|
private Handler uiHandler = new Handler(Looper.getMainLooper()) {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_ANIMATION_START:
|
||||||
|
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||||
|
animationCallback.onAnimationStart(FrameAnimationDrawable.this);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MSG_ANIMATION_END:
|
||||||
|
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||||
|
animationCallback.onAnimationEnd(FrameAnimationDrawable.this);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private Runnable invalidateRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private boolean autoPlay = true;
|
||||||
|
|
||||||
|
public FrameAnimationDrawable(Decoder frameSeqDecoder) {
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
this.frameSeqDecoder = frameSeqDecoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FrameAnimationDrawable(Loader provider) {
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
this.frameSeqDecoder = createFrameSeqDecoder(provider, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutoPlay(boolean autoPlay) {
|
||||||
|
this.autoPlay = autoPlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Decoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param loopLimit <=0为无限播放,>0为实际播放次数
|
||||||
|
*/
|
||||||
|
public void setLoopLimit(int loopLimit) {
|
||||||
|
frameSeqDecoder.setLoopLimit(loopLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
frameSeqDecoder.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
frameSeqDecoder.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resume() {
|
||||||
|
frameSeqDecoder.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPaused() {
|
||||||
|
return frameSeqDecoder.isPaused();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
if (autoPlay) {
|
||||||
|
frameSeqDecoder.start();
|
||||||
|
} else {
|
||||||
|
this.frameSeqDecoder.addRenderListener(this);
|
||||||
|
if (!this.frameSeqDecoder.isRunning()) {
|
||||||
|
this.frameSeqDecoder.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
if (autoPlay) {
|
||||||
|
frameSeqDecoder.stop();
|
||||||
|
} else {
|
||||||
|
this.frameSeqDecoder.removeRenderListener(this);
|
||||||
|
this.frameSeqDecoder.stopIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRunning() {
|
||||||
|
return frameSeqDecoder.isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(Canvas canvas) {
|
||||||
|
if (bitmap == null || bitmap.isRecycled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.setDrawFilter(drawFilter);
|
||||||
|
canvas.drawBitmap(bitmap, matrix, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBounds(int left, int top, int right, int bottom) {
|
||||||
|
super.setBounds(left, top, right, bottom);
|
||||||
|
boolean sampleSizeChanged = frameSeqDecoder.setDesiredSize(getBounds().width(), getBounds().height());
|
||||||
|
matrix.setScale(
|
||||||
|
1.0f * getBounds().width() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().width(),
|
||||||
|
1.0f * getBounds().height() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().height());
|
||||||
|
|
||||||
|
if (sampleSizeChanged)
|
||||||
|
this.bitmap = Bitmap.createBitmap(
|
||||||
|
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||||
|
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||||
|
Bitmap.Config.ARGB_8888);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
paint.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter colorFilter) {
|
||||||
|
paint.setColorFilter(colorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return PixelFormat.TRANSLUCENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
Message.obtain(uiHandler, MSG_ANIMATION_START).sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRender(ByteBuffer byteBuffer) {
|
||||||
|
if (!isRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.bitmap == null || this.bitmap.isRecycled()) {
|
||||||
|
this.bitmap = Bitmap.createBitmap(
|
||||||
|
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||||
|
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||||
|
Bitmap.Config.ARGB_8888);
|
||||||
|
}
|
||||||
|
byteBuffer.rewind();
|
||||||
|
if (byteBuffer.remaining() < this.bitmap.getByteCount()) {
|
||||||
|
Log.e(TAG, "onRender:Buffer not large enough for pixels");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.bitmap.copyPixelsFromBuffer(byteBuffer);
|
||||||
|
uiHandler.post(invalidateRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnd() {
|
||||||
|
Message.obtain(uiHandler, MSG_ANIMATION_END).sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setVisible(boolean visible, boolean restart) {
|
||||||
|
if (this.autoPlay) {
|
||||||
|
if (visible) {
|
||||||
|
if (!isRunning()) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
} else if (isRunning()) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.setVisible(visible, restart);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicWidth() {
|
||||||
|
try {
|
||||||
|
return frameSeqDecoder.getBounds().width();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicHeight() {
|
||||||
|
try {
|
||||||
|
return frameSeqDecoder.getBounds().height();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||||
|
this.animationCallbacks.add(animationCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unregisterAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||||
|
return this.animationCallbacks.remove(animationCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAnimationCallbacks() {
|
||||||
|
this.animationCallbacks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.decode;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
import org.signal.glide.common.io.Writer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: One frame in an animation
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-13
|
||||||
|
*/
|
||||||
|
public abstract class Frame<R extends Reader, W extends Writer> {
|
||||||
|
protected final R reader;
|
||||||
|
public int frameWidth;
|
||||||
|
public int frameHeight;
|
||||||
|
public int frameX;
|
||||||
|
public int frameY;
|
||||||
|
public int frameDuration;
|
||||||
|
|
||||||
|
public Frame(R reader) {
|
||||||
|
this.reader = reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer);
|
||||||
|
}
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.decode;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.signal.glide.Log;
|
||||||
|
import org.signal.glide.common.executor.FrameDecoderExecutor;
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
import org.signal.glide.common.io.Writer;
|
||||||
|
import org.signal.glide.common.loader.Loader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: Abstract Frame Animation Decoder
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/27
|
||||||
|
*/
|
||||||
|
public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
|
||||||
|
private static final String TAG = FrameSeqDecoder.class.getSimpleName();
|
||||||
|
private final int taskId;
|
||||||
|
|
||||||
|
private final Loader mLoader;
|
||||||
|
private final Handler workerHandler;
|
||||||
|
protected List<Frame> frames = new ArrayList<>();
|
||||||
|
protected int frameIndex = -1;
|
||||||
|
private int playCount;
|
||||||
|
private Integer loopLimit = null;
|
||||||
|
private Set<RenderListener> renderListeners = new HashSet<>();
|
||||||
|
private AtomicBoolean paused = new AtomicBoolean(true);
|
||||||
|
private static final Rect RECT_EMPTY = new Rect();
|
||||||
|
private Runnable renderTask = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (paused.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (canStep()) {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
long delay = step();
|
||||||
|
long cost = System.currentTimeMillis() - start;
|
||||||
|
workerHandler.postDelayed(this, Math.max(0, delay - cost));
|
||||||
|
for (RenderListener renderListener : renderListeners) {
|
||||||
|
renderListener.onRender(frameBuffer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
protected int sampleSize = 1;
|
||||||
|
|
||||||
|
private Set<Bitmap> cacheBitmaps = new HashSet<>();
|
||||||
|
protected Map<Bitmap, Canvas> cachedCanvas = new WeakHashMap<>();
|
||||||
|
protected ByteBuffer frameBuffer;
|
||||||
|
protected volatile Rect fullRect;
|
||||||
|
private W mWriter = getWriter();
|
||||||
|
private R mReader = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If played all the needed
|
||||||
|
*/
|
||||||
|
private boolean finished = false;
|
||||||
|
|
||||||
|
private enum State {
|
||||||
|
IDLE,
|
||||||
|
RUNNING,
|
||||||
|
INITIALIZING,
|
||||||
|
FINISHING,
|
||||||
|
}
|
||||||
|
|
||||||
|
private volatile State mState = State.IDLE;
|
||||||
|
|
||||||
|
public Loader getLoader() {
|
||||||
|
return mLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract W getWriter();
|
||||||
|
|
||||||
|
protected abstract R getReader(Reader reader);
|
||||||
|
|
||||||
|
protected Bitmap obtainBitmap(int width, int height) {
|
||||||
|
Bitmap ret = null;
|
||||||
|
Iterator<Bitmap> iterator = cacheBitmaps.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
int reuseSize = width * height * 4;
|
||||||
|
ret = iterator.next();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
if (ret != null && ret.getAllocationByteCount() >= reuseSize) {
|
||||||
|
iterator.remove();
|
||||||
|
if (ret.getWidth() != width || ret.getHeight() != height) {
|
||||||
|
ret.reconfigure(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
}
|
||||||
|
ret.eraseColor(0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ret != null && ret.getByteCount() >= reuseSize) {
|
||||||
|
if (ret.getWidth() == width && ret.getHeight() == height) {
|
||||||
|
iterator.remove();
|
||||||
|
ret.eraseColor(0);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Bitmap.Config config = Bitmap.Config.ARGB_8888;
|
||||||
|
ret = Bitmap.createBitmap(width, height, config);
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void recycleBitmap(Bitmap bitmap) {
|
||||||
|
if (bitmap != null && !cacheBitmaps.contains(bitmap)) {
|
||||||
|
cacheBitmaps.add(bitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解码器的渲染回调
|
||||||
|
*/
|
||||||
|
public interface RenderListener {
|
||||||
|
/**
|
||||||
|
* 播放开始
|
||||||
|
*/
|
||||||
|
void onStart();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 帧播放
|
||||||
|
*/
|
||||||
|
void onRender(ByteBuffer byteBuffer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放结束
|
||||||
|
*/
|
||||||
|
void onEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param loader webp的reader
|
||||||
|
* @param renderListener 渲染的回调
|
||||||
|
*/
|
||||||
|
public FrameSeqDecoder(Loader loader, @Nullable RenderListener renderListener) {
|
||||||
|
this.mLoader = loader;
|
||||||
|
if (renderListener != null) {
|
||||||
|
this.renderListeners.add(renderListener);
|
||||||
|
}
|
||||||
|
this.taskId = FrameDecoderExecutor.getInstance().generateTaskId();
|
||||||
|
this.workerHandler = new Handler(FrameDecoderExecutor.getInstance().getLooper(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void addRenderListener(final RenderListener renderListener) {
|
||||||
|
this.workerHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
renderListeners.add(renderListener);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeRenderListener(final RenderListener renderListener) {
|
||||||
|
this.workerHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
renderListeners.remove(renderListener);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopIfNeeded() {
|
||||||
|
this.workerHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (renderListeners.size() == 0) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Rect getBounds() {
|
||||||
|
if (fullRect == null) {
|
||||||
|
if (mState == State.FINISHING) {
|
||||||
|
Log.e(TAG, "In finishing,do not interrupt");
|
||||||
|
}
|
||||||
|
final Thread thread = Thread.currentThread();
|
||||||
|
workerHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
if (fullRect == null) {
|
||||||
|
if (mReader == null) {
|
||||||
|
mReader = getReader(mLoader.obtain());
|
||||||
|
} else {
|
||||||
|
mReader.reset();
|
||||||
|
}
|
||||||
|
initCanvasBounds(read(mReader));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fullRect = RECT_EMPTY;
|
||||||
|
} finally {
|
||||||
|
LockSupport.unpark(thread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
LockSupport.park(thread);
|
||||||
|
}
|
||||||
|
return fullRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCanvasBounds(Rect rect) {
|
||||||
|
fullRect = rect;
|
||||||
|
frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4);
|
||||||
|
if (mWriter == null) {
|
||||||
|
mWriter = getWriter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private int getFrameCount() {
|
||||||
|
return this.frames.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Loop Count defined in file
|
||||||
|
*/
|
||||||
|
protected abstract int getLoopCount();
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (fullRect == RECT_EMPTY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mState == State.RUNNING || mState == State.INITIALIZING) {
|
||||||
|
Log.i(TAG, debugInfo() + " Already started");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mState == State.FINISHING) {
|
||||||
|
Log.e(TAG, debugInfo() + " Processing,wait for finish at " + mState);
|
||||||
|
}
|
||||||
|
mState = State.INITIALIZING;
|
||||||
|
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||||
|
innerStart();
|
||||||
|
} else {
|
||||||
|
workerHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
innerStart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private void innerStart() {
|
||||||
|
paused.compareAndSet(true, false);
|
||||||
|
|
||||||
|
final long start = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
if (frames.size() == 0) {
|
||||||
|
try {
|
||||||
|
if (mReader == null) {
|
||||||
|
mReader = getReader(mLoader.obtain());
|
||||||
|
} else {
|
||||||
|
mReader.reset();
|
||||||
|
}
|
||||||
|
initCanvasBounds(read(mReader));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Log.i(TAG, debugInfo() + " Set state to RUNNING,cost " + (System.currentTimeMillis() - start));
|
||||||
|
mState = State.RUNNING;
|
||||||
|
}
|
||||||
|
if (getNumPlays() == 0 || !finished) {
|
||||||
|
this.frameIndex = -1;
|
||||||
|
renderTask.run();
|
||||||
|
for (RenderListener renderListener : renderListeners) {
|
||||||
|
renderListener.onStart();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, debugInfo() + " No need to started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private void innerStop() {
|
||||||
|
workerHandler.removeCallbacks(renderTask);
|
||||||
|
frames.clear();
|
||||||
|
for (Bitmap bitmap : cacheBitmaps) {
|
||||||
|
if (bitmap != null && !bitmap.isRecycled()) {
|
||||||
|
bitmap.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cacheBitmaps.clear();
|
||||||
|
if (frameBuffer != null) {
|
||||||
|
frameBuffer = null;
|
||||||
|
}
|
||||||
|
cachedCanvas.clear();
|
||||||
|
try {
|
||||||
|
if (mReader != null) {
|
||||||
|
mReader.close();
|
||||||
|
mReader = null;
|
||||||
|
}
|
||||||
|
if (mWriter != null) {
|
||||||
|
mWriter.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
release();
|
||||||
|
mState = State.IDLE;
|
||||||
|
for (RenderListener renderListener : renderListeners) {
|
||||||
|
renderListener.onEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (fullRect == RECT_EMPTY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mState == State.FINISHING || mState == State.IDLE) {
|
||||||
|
Log.i(TAG, debugInfo() + "No need to stop");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mState == State.INITIALIZING) {
|
||||||
|
Log.e(TAG, debugInfo() + "Processing,wait for finish at " + mState);
|
||||||
|
}
|
||||||
|
mState = State.FINISHING;
|
||||||
|
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||||
|
innerStop();
|
||||||
|
} else {
|
||||||
|
workerHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
innerStop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String debugInfo() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void release();
|
||||||
|
|
||||||
|
public boolean isRunning() {
|
||||||
|
return mState == State.RUNNING || mState == State.INITIALIZING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPaused() {
|
||||||
|
return paused.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoopLimit(int limit) {
|
||||||
|
this.loopLimit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
this.playCount = 0;
|
||||||
|
this.frameIndex = -1;
|
||||||
|
this.finished = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
workerHandler.removeCallbacks(renderTask);
|
||||||
|
paused.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resume() {
|
||||||
|
paused.compareAndSet(true, false);
|
||||||
|
workerHandler.removeCallbacks(renderTask);
|
||||||
|
workerHandler.post(renderTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getSampleSize() {
|
||||||
|
return sampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setDesiredSize(int width, int height) {
|
||||||
|
boolean sampleSizeChanged = false;
|
||||||
|
int sample = getDesiredSample(width, height);
|
||||||
|
if (sample != this.sampleSize) {
|
||||||
|
this.sampleSize = sample;
|
||||||
|
sampleSizeChanged = true;
|
||||||
|
final boolean tempRunning = isRunning();
|
||||||
|
workerHandler.removeCallbacks(renderTask);
|
||||||
|
workerHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
innerStop();
|
||||||
|
try {
|
||||||
|
initCanvasBounds(read(getReader(mLoader.obtain())));
|
||||||
|
if (tempRunning) {
|
||||||
|
innerStart();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sampleSizeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getDesiredSample(int desiredWidth, int desiredHeight) {
|
||||||
|
if (desiredWidth == 0 || desiredHeight == 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int radio = Math.min(getBounds().width() / desiredWidth, getBounds().height() / desiredHeight);
|
||||||
|
int sample = 1;
|
||||||
|
while ((sample * 2) <= radio) {
|
||||||
|
sample *= 2;
|
||||||
|
}
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Rect read(R reader) throws IOException;
|
||||||
|
|
||||||
|
private int getNumPlays() {
|
||||||
|
return this.loopLimit != null ? this.loopLimit : this.getLoopCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canStep() {
|
||||||
|
if (!isRunning()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (frames.size() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getNumPlays() <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.playCount < getNumPlays() - 1) {
|
||||||
|
return true;
|
||||||
|
} else if (this.playCount == getNumPlays() - 1 && this.frameIndex < this.getFrameCount() - 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finished = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private long step() {
|
||||||
|
this.frameIndex++;
|
||||||
|
if (this.frameIndex >= this.getFrameCount()) {
|
||||||
|
this.frameIndex = 0;
|
||||||
|
this.playCount++;
|
||||||
|
}
|
||||||
|
Frame frame = getFrame(this.frameIndex);
|
||||||
|
if (frame == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
renderFrame(frame);
|
||||||
|
return frame.frameDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void renderFrame(Frame frame);
|
||||||
|
|
||||||
|
private Frame getFrame(int index) {
|
||||||
|
if (index < 0 || index >= frames.size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return frames.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Indexed frame
|
||||||
|
*
|
||||||
|
* @param index <0 means reverse from last index
|
||||||
|
*/
|
||||||
|
public Bitmap getFrameBitmap(int index) throws IOException {
|
||||||
|
if (mState != State.IDLE) {
|
||||||
|
Log.e(TAG, debugInfo() + ",stop first");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
mState = State.RUNNING;
|
||||||
|
paused.compareAndSet(true, false);
|
||||||
|
if (frames.size() == 0) {
|
||||||
|
if (mReader == null) {
|
||||||
|
mReader = getReader(mLoader.obtain());
|
||||||
|
} else {
|
||||||
|
mReader.reset();
|
||||||
|
}
|
||||||
|
initCanvasBounds(read(mReader));
|
||||||
|
}
|
||||||
|
if (index < 0) {
|
||||||
|
index += this.frames.size();
|
||||||
|
}
|
||||||
|
if (index < 0) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
frameIndex = -1;
|
||||||
|
while (frameIndex < index) {
|
||||||
|
if (canStep()) {
|
||||||
|
step();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frameBuffer.rewind();
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(getBounds().width() / getSampleSize(), getBounds().height() / getSampleSize(), Bitmap.Config.ARGB_8888);
|
||||||
|
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||||
|
innerStop();
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.executor;
|
||||||
|
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: com.github.penfeizhou.animation.executor
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-11-21
|
||||||
|
*/
|
||||||
|
public class FrameDecoderExecutor {
|
||||||
|
private static int sPoolNumber = 4;
|
||||||
|
private ArrayList<HandlerThread> mHandlerThreadGroup = new ArrayList<>();
|
||||||
|
private AtomicInteger counter = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private FrameDecoderExecutor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Inner {
|
||||||
|
static final FrameDecoderExecutor sInstance = new FrameDecoderExecutor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPoolSize(int size) {
|
||||||
|
sPoolNumber = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FrameDecoderExecutor getInstance() {
|
||||||
|
return Inner.sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Looper getLooper(int taskId) {
|
||||||
|
int idx = taskId % sPoolNumber;
|
||||||
|
if (idx >= mHandlerThreadGroup.size()) {
|
||||||
|
HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx);
|
||||||
|
handlerThread.start();
|
||||||
|
|
||||||
|
mHandlerThreadGroup.add(handlerThread);
|
||||||
|
Looper looper = handlerThread.getLooper();
|
||||||
|
if (looper != null) {
|
||||||
|
return looper;
|
||||||
|
} else {
|
||||||
|
return Looper.getMainLooper();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mHandlerThreadGroup.get(idx) != null) {
|
||||||
|
Looper looper = mHandlerThreadGroup.get(idx).getLooper();
|
||||||
|
if (looper != null) {
|
||||||
|
return looper;
|
||||||
|
} else {
|
||||||
|
return Looper.getMainLooper();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Looper.getMainLooper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int generateTaskId() {
|
||||||
|
return counter.getAndIncrement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.io;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: APNG4Android
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-14
|
||||||
|
*/
|
||||||
|
public class ByteBufferReader implements Reader {
|
||||||
|
|
||||||
|
private final ByteBuffer byteBuffer;
|
||||||
|
|
||||||
|
public ByteBufferReader(ByteBuffer byteBuffer) {
|
||||||
|
this.byteBuffer = byteBuffer;
|
||||||
|
byteBuffer.position(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long total) throws IOException {
|
||||||
|
byteBuffer.position((int) (byteBuffer.position() + total));
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte peek() throws IOException {
|
||||||
|
return byteBuffer.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() throws IOException {
|
||||||
|
byteBuffer.position(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int position() {
|
||||||
|
return byteBuffer.position();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||||
|
byteBuffer.get(buffer, start, byteCount);
|
||||||
|
return byteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return byteBuffer.limit() - byteBuffer.position();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream toInputStream() throws IOException {
|
||||||
|
return new ByteArrayInputStream(byteBuffer.array());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.io;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: ByteBufferWriter
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-12
|
||||||
|
*/
|
||||||
|
public class ByteBufferWriter implements Writer {
|
||||||
|
|
||||||
|
protected ByteBuffer byteBuffer;
|
||||||
|
|
||||||
|
public ByteBufferWriter() {
|
||||||
|
reset(10 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putByte(byte b) {
|
||||||
|
byteBuffer.put(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putBytes(byte[] b) {
|
||||||
|
byteBuffer.put(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int position() {
|
||||||
|
return byteBuffer.position();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void skip(int length) {
|
||||||
|
byteBuffer.position(length + position());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] toByteArray() {
|
||||||
|
return byteBuffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(int size) {
|
||||||
|
if (byteBuffer == null || size > byteBuffer.capacity()) {
|
||||||
|
byteBuffer = ByteBuffer.allocate(size);
|
||||||
|
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
}
|
||||||
|
byteBuffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.io;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: FileReader
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-23
|
||||||
|
*/
|
||||||
|
public class FileReader extends FilterReader {
|
||||||
|
private final File mFile;
|
||||||
|
|
||||||
|
public FileReader(File file) throws IOException {
|
||||||
|
super(new StreamReader(new FileInputStream(file)));
|
||||||
|
mFile = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() throws IOException {
|
||||||
|
reader.close();
|
||||||
|
reader = new StreamReader(new FileInputStream(mFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: FilterReader
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-23
|
||||||
|
*/
|
||||||
|
public class FilterReader implements Reader {
|
||||||
|
protected Reader reader;
|
||||||
|
|
||||||
|
public FilterReader(Reader in) {
|
||||||
|
this.reader = in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long total) throws IOException {
|
||||||
|
return reader.skip(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte peek() throws IOException {
|
||||||
|
return reader.peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() throws IOException {
|
||||||
|
reader.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int position() {
|
||||||
|
return reader.position();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||||
|
return reader.read(buffer, start, byteCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return reader.available();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream toInputStream() throws IOException {
|
||||||
|
reset();
|
||||||
|
return reader.toInputStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link {https://developers.google.com/speed/webp/docs/riff_container#terminology_basics}
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-11
|
||||||
|
*/
|
||||||
|
public interface Reader {
|
||||||
|
long skip(long total) throws IOException;
|
||||||
|
|
||||||
|
byte peek() throws IOException;
|
||||||
|
|
||||||
|
void reset() throws IOException;
|
||||||
|
|
||||||
|
int position();
|
||||||
|
|
||||||
|
int read(byte[] buffer, int start, int byteCount) throws IOException;
|
||||||
|
|
||||||
|
int available() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* close io
|
||||||
|
*/
|
||||||
|
void close() throws IOException;
|
||||||
|
|
||||||
|
InputStream toInputStream() throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.io;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-11
|
||||||
|
*/
|
||||||
|
public class StreamReader extends FilterInputStream implements Reader {
|
||||||
|
private int position;
|
||||||
|
|
||||||
|
public StreamReader(InputStream in) {
|
||||||
|
super(in);
|
||||||
|
try {
|
||||||
|
in.reset();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte peek() throws IOException {
|
||||||
|
byte ret = (byte) read();
|
||||||
|
position++;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
int ret = super.read(b, off, len);
|
||||||
|
position += Math.max(0, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void reset() throws IOException {
|
||||||
|
super.reset();
|
||||||
|
position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
long ret = super.skip(n);
|
||||||
|
position += ret;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int position() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream toInputStream() throws IOException {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: APNG4Android
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-12
|
||||||
|
*/
|
||||||
|
public interface Writer {
|
||||||
|
void reset(int size);
|
||||||
|
|
||||||
|
void putByte(byte b);
|
||||||
|
|
||||||
|
void putBytes(byte[] b);
|
||||||
|
|
||||||
|
int position();
|
||||||
|
|
||||||
|
void skip(int length);
|
||||||
|
|
||||||
|
byte[] toByteArray();
|
||||||
|
|
||||||
|
void close() throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.loader;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 从Asset中读取流
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/28
|
||||||
|
*/
|
||||||
|
public class AssetStreamLoader extends StreamLoader {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final String mAssetName;
|
||||||
|
|
||||||
|
public AssetStreamLoader(Context context, String assetName) {
|
||||||
|
mContext = context.getApplicationContext();
|
||||||
|
mAssetName = assetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected InputStream getInputStream() throws IOException {
|
||||||
|
return mContext.getAssets().open(mAssetName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.loader;
|
||||||
|
|
||||||
|
import org.signal.glide.common.io.ByteBufferReader;
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: ByteBufferLoader
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-15
|
||||||
|
*/
|
||||||
|
public abstract class ByteBufferLoader implements Loader {
|
||||||
|
public abstract ByteBuffer getByteBuffer();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Reader obtain() throws IOException {
|
||||||
|
return new ByteBufferReader(getByteBuffer());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.loader;
|
||||||
|
|
||||||
|
import org.signal.glide.common.io.FileReader;
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 从文件加载流
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/28
|
||||||
|
*/
|
||||||
|
public class FileLoader implements Loader {
|
||||||
|
|
||||||
|
private final File mFile;
|
||||||
|
private Reader mReader;
|
||||||
|
|
||||||
|
public FileLoader(String path) {
|
||||||
|
mFile = new File(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized Reader obtain() throws IOException {
|
||||||
|
return new FileReader(mFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.loader;
|
||||||
|
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: Loader
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019-05-14
|
||||||
|
*/
|
||||||
|
public interface Loader {
|
||||||
|
Reader obtain() throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.loader;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 从资源加载流
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/28
|
||||||
|
*/
|
||||||
|
public class ResourceStreamLoader extends StreamLoader {
|
||||||
|
private final Context mContext;
|
||||||
|
private final int mResId;
|
||||||
|
|
||||||
|
|
||||||
|
public ResourceStreamLoader(Context context, int resId) {
|
||||||
|
mContext = context.getApplicationContext();
|
||||||
|
mResId = resId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected InputStream getInputStream() throws IOException {
|
||||||
|
return mContext.getResources().openRawResource(mResId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Zhou Pengfei
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.glide.common.loader;
|
||||||
|
|
||||||
|
import org.signal.glide.common.io.Reader;
|
||||||
|
import org.signal.glide.common.io.StreamReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: pengfei.zhou
|
||||||
|
* @CreateDate: 2019/3/28
|
||||||
|
*/
|
||||||
|
public abstract class StreamLoader implements Loader {
|
||||||
|
protected abstract InputStream getInputStream() throws IOException;
|
||||||
|
|
||||||
|
|
||||||
|
public final synchronized Reader obtain() throws IOException {
|
||||||
|
return new StreamReader(getInputStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||||
|
|
||||||
public final class AppCapabilities {
|
public final class AppCapabilities {
|
||||||
|
|
||||||
@@ -9,12 +9,13 @@ public final class AppCapabilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final boolean UUID_CAPABLE = false;
|
private static final boolean UUID_CAPABLE = false;
|
||||||
|
private static final boolean GV2_CAPABLE = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param storageCapable Whether or not the user can use storage service. This is another way of
|
* @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.
|
* asking if the user has set a Signal PIN or not.
|
||||||
*/
|
*/
|
||||||
public static SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) {
|
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||||
return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable);
|
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, FeatureFlags.groupsV1AutoMigration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ public final class AppInitialization {
|
|||||||
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||||
|
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||||
|
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||||
|
TextSecurePreferences.setReadReceiptsEnabled(context, true);
|
||||||
|
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
|
||||||
|
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
|
||||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||||
SignalStore.onFirstEverAppLaunch();
|
SignalStore.onFirstEverAppLaunch();
|
||||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import android.os.AsyncTask;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
@@ -32,9 +33,8 @@ import com.google.android.gms.security.ProviderInstaller;
|
|||||||
|
|
||||||
import org.conscrypt.Conscrypt;
|
import org.conscrypt.Conscrypt;
|
||||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||||
|
import org.signal.glide.SignalGlideCodecs;
|
||||||
import org.signal.ringrtc.CallManager;
|
import org.signal.ringrtc.CallManager;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
@@ -42,10 +42,12 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
|||||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||||
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.GroupV1MigrationJob;
|
||||||
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.RefreshPreKeysJob;
|
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
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;
|
||||||
@@ -66,8 +68,11 @@ 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.storage.StorageSyncHelper;
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
|
import org.thoughtcrime.securesms.tracing.Trace;
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
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;
|
||||||
@@ -87,15 +92,14 @@ import java.util.concurrent.TimeUnit;
|
|||||||
*
|
*
|
||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*/
|
*/
|
||||||
|
@Trace
|
||||||
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
|
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
|
||||||
|
|
||||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||||
|
|
||||||
private ExpiringMessageManager expiringMessageManager;
|
private ExpiringMessageManager expiringMessageManager;
|
||||||
private ViewOnceMessageManager viewOnceMessageManager;
|
private ViewOnceMessageManager viewOnceMessageManager;
|
||||||
private TypingStatusRepository typingStatusRepository;
|
private PersistentLogger persistentLogger;
|
||||||
private TypingStatusSender typingStatusSender;
|
|
||||||
private PersistentLogger persistentLogger;
|
|
||||||
|
|
||||||
private volatile boolean isAppVisible;
|
private volatile boolean isAppVisible;
|
||||||
|
|
||||||
@@ -117,8 +121,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
initializeMessageRetrieval();
|
initializeMessageRetrieval();
|
||||||
initializeExpiringMessageManager();
|
initializeExpiringMessageManager();
|
||||||
initializeRevealableMessageManager();
|
initializeRevealableMessageManager();
|
||||||
initializeTypingStatusRepository();
|
|
||||||
initializeTypingStatusSender();
|
|
||||||
initializeGcmCheck();
|
initializeGcmCheck();
|
||||||
initializeSignedPreKeyCheck();
|
initializeSignedPreKeyCheck();
|
||||||
initializePeriodicTasks();
|
initializePeriodicTasks();
|
||||||
@@ -127,12 +129,12 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
initializePendingMessages();
|
initializePendingMessages();
|
||||||
initializeBlobProvider();
|
initializeBlobProvider();
|
||||||
initializeCleanup();
|
initializeCleanup();
|
||||||
|
initializeGlideCodecs();
|
||||||
|
|
||||||
FeatureFlags.init();
|
FeatureFlags.init();
|
||||||
NotificationChannels.create(this);
|
NotificationChannels.create(this);
|
||||||
RefreshPreKeysJob.scheduleIfNecessary();
|
RefreshPreKeysJob.scheduleIfNecessary();
|
||||||
StorageSyncHelper.scheduleRoutineSync();
|
StorageSyncHelper.scheduleRoutineSync();
|
||||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
|
||||||
RegistrationUtil.maybeMarkRegistrationComplete(this);
|
RegistrationUtil.maybeMarkRegistrationComplete(this);
|
||||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||||
|
|
||||||
@@ -141,6 +143,9 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||||
|
|
||||||
|
DynamicTheme.setDefaultDayNightMode(this);
|
||||||
|
|
||||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,10 +155,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
Log.i(TAG, "App is now visible.");
|
Log.i(TAG, "App is now visible.");
|
||||||
FeatureFlags.refreshIfNecessary();
|
FeatureFlags.refreshIfNecessary();
|
||||||
ApplicationDependencies.getRecipientCache().warmUp();
|
ApplicationDependencies.getRecipientCache().warmUp();
|
||||||
|
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||||
|
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
|
||||||
executePendingContactSync();
|
executePendingContactSync();
|
||||||
KeyCachingService.onAppForegrounded(this);
|
KeyCachingService.onAppForegrounded(this);
|
||||||
ApplicationDependencies.getFrameRateTracker().begin();
|
ApplicationDependencies.getFrameRateTracker().begin();
|
||||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||||
|
checkBuildExpiration();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -173,14 +181,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
return viewOnceMessageManager;
|
return viewOnceMessageManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TypingStatusRepository getTypingStatusRepository() {
|
|
||||||
return typingStatusRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TypingStatusSender getTypingStatusSender() {
|
|
||||||
return typingStatusSender;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAppVisible() {
|
public boolean isAppVisible() {
|
||||||
return isAppVisible;
|
return isAppVisible;
|
||||||
}
|
}
|
||||||
@@ -189,6 +189,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
return persistentLogger;
|
return persistentLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void checkBuildExpiration() {
|
||||||
|
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||||
|
Log.w(TAG, "Build expired!");
|
||||||
|
SignalStore.misc().markClientDeprecated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void initializeSecurityProvider() {
|
private void initializeSecurityProvider() {
|
||||||
try {
|
try {
|
||||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||||
@@ -273,14 +280,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
this.viewOnceMessageManager = new ViewOnceMessageManager(this);
|
this.viewOnceMessageManager = new ViewOnceMessageManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeTypingStatusRepository() {
|
|
||||||
this.typingStatusRepository = new TypingStatusRepository();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeTypingStatusSender() {
|
|
||||||
this.typingStatusSender = new TypingStatusSender(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializePeriodicTasks() {
|
private void initializePeriodicTasks() {
|
||||||
RotateSignedPreKeyListener.schedule(this);
|
RotateSignedPreKeyListener.schedule(this);
|
||||||
DirectoryRefreshListener.schedule(this);
|
DirectoryRefreshListener.schedule(this);
|
||||||
@@ -378,9 +377,39 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeGlideCodecs() {
|
||||||
|
SignalGlideCodecs.setLogProvider(new org.signal.glide.Log.Provider() {
|
||||||
|
@Override
|
||||||
|
public void v(@NonNull String tag, @NonNull String message) {
|
||||||
|
Log.v(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void d(@NonNull String tag, @NonNull String message) {
|
||||||
|
Log.d(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void i(@NonNull String tag, @NonNull String message) {
|
||||||
|
Log.i(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void w(@NonNull String tag, @NonNull String message) {
|
||||||
|
Log.w(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||||
|
Log.e(tag, message, throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void attachBaseContext(Context base) {
|
protected void attachBaseContext(Context base) {
|
||||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
DynamicLanguageContextWrapper.updateContext(base);
|
||||||
|
super.attachBaseContext(base);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ProviderInitializationException extends RuntimeException {
|
private static class ProviderInitializationException extends RuntimeException {
|
||||||
|
|||||||
@@ -17,32 +17,40 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.help.HelpFragment;
|
import org.thoughtcrime.securesms.help.HelpFragment;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
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;
|
||||||
|
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
|
||||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
||||||
|
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
|
||||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
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.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
|
|
||||||
@@ -56,10 +64,13 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
|||||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
{
|
{
|
||||||
|
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
|
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
|
||||||
|
|
||||||
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
|
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
|
||||||
|
private static final String PREFERENCE_CATEGORY_USERNAME = "preference_category_username";
|
||||||
private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms";
|
private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms";
|
||||||
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
|
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
|
||||||
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
|
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
|
||||||
@@ -69,10 +80,15 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
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_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 static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
||||||
|
|
||||||
|
private static final String WAS_CONFIGURATION_UPDATED = "was_configuration_updated";
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||||
|
|
||||||
|
private boolean wasConfigurationUpdated = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPreCreate() {
|
protected void onPreCreate() {
|
||||||
dynamicTheme.onCreate(this);
|
dynamicTheme.onCreate(this);
|
||||||
@@ -86,11 +102,21 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
|
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
|
||||||
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
|
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
|
||||||
|
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
|
||||||
|
initFragment(android.R.id.content, new BackupsPreferenceFragment());
|
||||||
} else if (icicle == null) {
|
} else if (icicle == null) {
|
||||||
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
|
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
|
||||||
|
} else {
|
||||||
|
wasConfigurationUpdated = icicle.getBoolean(WAS_CONFIGURATION_UPDATED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
|
outState.putBoolean(WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated);
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@@ -112,20 +138,28 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
if (fragmentManager.getBackStackEntryCount() > 0) {
|
if (fragmentManager.getBackStackEntryCount() > 0) {
|
||||||
fragmentManager.popBackStack();
|
fragmentManager.popBackStack();
|
||||||
} else {
|
} else {
|
||||||
// TODO [greyson] Navigation
|
if (wasConfigurationUpdated) {
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
setResult(MainActivity.RESULT_CONFIG_CHANGED);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
} else {
|
||||||
startActivity(intent);
|
setResult(RESULT_OK);
|
||||||
|
}
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
onSupportNavigateUp();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||||
if (key.equals(TextSecurePreferences.THEME_PREF)) {
|
if (key.equals(TextSecurePreferences.THEME_PREF)) {
|
||||||
|
DynamicTheme.setDefaultDayNightMode(this);
|
||||||
recreate();
|
recreate();
|
||||||
} else if (key.equals(TextSecurePreferences.LANGUAGE_PREF)) {
|
} else if (key.equals(TextSecurePreferences.LANGUAGE_PREF)) {
|
||||||
|
wasConfigurationUpdated = true;
|
||||||
recreate();
|
recreate();
|
||||||
|
|
||||||
Intent intent = new Intent(this, KeyCachingService.class);
|
Intent intent = new Intent(this, KeyCachingService.class);
|
||||||
@@ -134,6 +168,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void pushFragment(@NonNull Fragment fragment) {
|
||||||
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||||
|
.replace(android.R.id.content, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
|
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -142,6 +184,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
|
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
|
||||||
.setOnPreferenceClickListener(new ProfileClickListener());
|
.setOnPreferenceClickListener(new ProfileClickListener());
|
||||||
|
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
||||||
|
.setOnPreferenceClickListener(new UsernameClickListener());
|
||||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
||||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||||
@@ -159,7 +203,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(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));
|
||||||
|
this.findPreference(PREFERENCE_CATEGORY_DONATE)
|
||||||
|
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
|
||||||
|
|
||||||
tintIcons();
|
tintIcons();
|
||||||
}
|
}
|
||||||
@@ -168,12 +214,30 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
if (Build.VERSION.SDK_INT >= 21) return;
|
if (Build.VERSION.SDK_INT >= 21) return;
|
||||||
|
|
||||||
Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS);
|
Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS);
|
||||||
preference.getIcon().setColorFilter(ThemeUtil.getThemedColor(requireContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_IN);
|
preference.getIcon().setColorFilter(ContextCompat.getColor(requireContext(), R.color.signal_icon_tint_primary), PorterDuff.Mode.SRC_IN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
|
||||||
|
if (FeatureFlags.usernames()) {
|
||||||
|
UsernamePreference pref = (UsernamePreference) findPreference(PREFERENCE_CATEGORY_USERNAME);
|
||||||
|
pref.setVisible(shouldDisplayUsernameReminder());
|
||||||
|
pref.setOnLongClickListener(v -> {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setMessage(R.string.ApplicationPreferencesActivity_hide_reminder)
|
||||||
|
.setPositiveButton(R.string.ApplicationPreferencesActivity_hide, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
SignalStore.misc().hideUsernameReminder();
|
||||||
|
findPreference(PREFERENCE_CATEGORY_USERNAME).setVisible(false);
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||||
|
.setCancelable(true)
|
||||||
|
.show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -188,6 +252,11 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
private void setCategorySummaries() {
|
private void setCategorySummaries() {
|
||||||
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
|
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
|
||||||
|
|
||||||
|
if (FeatureFlags.usernames()) {
|
||||||
|
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
||||||
|
.setVisible(shouldDisplayUsernameReminder());
|
||||||
|
}
|
||||||
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||||
.setSummary(SmsMmsPreferenceFragment.getSummary(getActivity()));
|
.setSummary(SmsMmsPreferenceFragment.getSummary(getActivity()));
|
||||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||||
@@ -207,6 +276,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean shouldDisplayUsernameReminder() {
|
||||||
|
return FeatureFlags.usernames() && !Recipient.self().getUsername().isPresent() && SignalStore.misc().shouldShowUsernameReminder();
|
||||||
|
}
|
||||||
|
|
||||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@@ -247,6 +320,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
case PREFERENCE_CATEGORY_HELP:
|
case PREFERENCE_CATEGORY_HELP:
|
||||||
fragment = new HelpFragment();
|
fragment = new HelpFragment();
|
||||||
break;
|
break;
|
||||||
|
case PREFERENCE_CATEGORY_DONATE:
|
||||||
|
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
@@ -255,14 +331,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
|
|
||||||
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
|
||||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
|
||||||
|
|
||||||
fragmentTransaction.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end);
|
|
||||||
|
|
||||||
fragmentTransaction.replace(android.R.id.content, fragment);
|
|
||||||
fragmentTransaction.addToBackStack(null);
|
|
||||||
fragmentTransaction.commit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -276,6 +345,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
|
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import android.os.Build;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.transition.TransitionInflater;
|
import android.transition.TransitionInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -36,6 +34,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for displaying avatars full screen.
|
* Activity for displaying avatars full screen.
|
||||||
@@ -72,27 +71,21 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
|||||||
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
|
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
|
||||||
}
|
}
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
ImageView avatar = findViewById(R.id.avatar);
|
||||||
ImageView avatar = findViewById(R.id.avatar);
|
|
||||||
|
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
||||||
|
|
||||||
showSystemUI();
|
|
||||||
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
|
|
||||||
Context context = getApplicationContext();
|
Context context = getApplicationContext();
|
||||||
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
||||||
|
|
||||||
Recipient.live(recipientId).observe(this, recipient -> {
|
Recipient.live(recipientId).observe(this, recipient -> {
|
||||||
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
ContactPhoto contactPhoto = recipient.isSelf() ? 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.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||||
: recipient.getFallbackContactPhoto();
|
: recipient.getFallbackContactPhoto();
|
||||||
|
|
||||||
Resources resources = this.getResources();
|
Resources resources = this.getResources();
|
||||||
|
|
||||||
@@ -132,47 +125,13 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
|||||||
toolbar.setTitle(recipient.getDisplayName(context));
|
toolbar.setTitle(recipient.getDisplayName(context));
|
||||||
});
|
});
|
||||||
|
|
||||||
avatar.setOnClickListener(v -> toggleUiVisibility());
|
FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
|
||||||
|
|
||||||
showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
|
||||||
}
|
|
||||||
|
|
||||||
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
|
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||||
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
|
|
||||||
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
|
|
||||||
|
|
||||||
for (View view : views) {
|
fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
||||||
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
|
@Override
|
||||||
|
|||||||
@@ -3,22 +3,28 @@ package org.thoughtcrime.securesms;
|
|||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.app.ActivityOptionsCompat;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.app.ActivityOptionsCompat;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.util.ConfigurationUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
|
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
|
||||||
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
|
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
|
||||||
@@ -37,7 +43,6 @@ public abstract class BaseActivity extends AppCompatActivity {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
initializeScreenshotSecurity();
|
initializeScreenshotSecurity();
|
||||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -72,19 +77,39 @@ public abstract class BaseActivity extends AppCompatActivity {
|
|||||||
ActivityCompat.startActivity(this, intent, bundle);
|
ActivityCompat.startActivity(this, intent, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
@Override
|
||||||
protected void setStatusBarColor(int color) {
|
protected void attachBaseContext(@NonNull Context newBase) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
super.attachBaseContext(newBase);
|
||||||
getWindow().setStatusBarColor(color);
|
|
||||||
}
|
Configuration configuration = new Configuration(newBase.getResources().getConfiguration());
|
||||||
|
int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode()
|
||||||
|
: AppCompatDelegate.getDefaultNightMode();
|
||||||
|
|
||||||
|
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
|
||||||
|
|
||||||
|
applyOverrideConfiguration(configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void attachBaseContext(Context newBase) {
|
public void applyOverrideConfiguration(@NonNull Configuration overrideConfiguration) {
|
||||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
DynamicLanguageContextWrapper.prepareOverrideConfiguration(this, overrideConfiguration);
|
||||||
|
super.applyOverrideConfiguration(overrideConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logEvent(@NonNull String event) {
|
private void logEvent(@NonNull String event) {
|
||||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final @NonNull ActionBar requireSupportActionBar() {
|
||||||
|
return Objects.requireNonNull(getSupportActionBar());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int mapNightModeToConfigurationUiMode(@NonNull Context context, @AppCompatDelegate.NightMode int appCompatNightMode) {
|
||||||
|
if (appCompatNightMode == AppCompatDelegate.MODE_NIGHT_YES) {
|
||||||
|
return Configuration.UI_MODE_NIGHT_YES;
|
||||||
|
} else if (appCompatNightMode == AppCompatDelegate.MODE_NIGHT_NO) {
|
||||||
|
return Configuration.UI_MODE_NIGHT_NO;
|
||||||
|
}
|
||||||
|
return ConfigurationUtil.getNightModeConfiguration(context.getApplicationContext());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.view.View;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||||
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.groups.GroupId;
|
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;
|
||||||
@@ -22,7 +26,8 @@ import java.util.Locale;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public interface BindableConversationItem extends Unbindable {
|
public interface BindableConversationItem extends Unbindable {
|
||||||
void bind(@NonNull ConversationMessage messageRecord,
|
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||||
|
@NonNull ConversationMessage messageRecord,
|
||||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||||
@NonNull GlideRequests glideRequests,
|
@NonNull GlideRequests glideRequests,
|
||||||
@@ -30,7 +35,7 @@ public interface BindableConversationItem extends Unbindable {
|
|||||||
@NonNull Set<ConversationMessage> batchSelected,
|
@NonNull Set<ConversationMessage> batchSelected,
|
||||||
@NonNull Recipient recipients,
|
@NonNull Recipient recipients,
|
||||||
@Nullable String searchQuery,
|
@Nullable String searchQuery,
|
||||||
boolean pulseHighlight);
|
boolean pulseMention);
|
||||||
|
|
||||||
ConversationMessage getConversationMessage();
|
ConversationMessage getConversationMessage();
|
||||||
|
|
||||||
@@ -49,5 +54,15 @@ public interface BindableConversationItem extends Unbindable {
|
|||||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||||
|
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||||
|
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||||
|
void onVoiceNotePause(@NonNull Uri uri);
|
||||||
|
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
|
||||||
|
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
|
||||||
|
void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients);
|
||||||
|
void onJoinGroupCallClicked();
|
||||||
|
|
||||||
|
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||||
|
boolean onUrlClicked(@NonNull String url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
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.loaders.BlockedContactsLoader;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.preferences.BlockedContactListItem;
|
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
|
||||||
|
|
||||||
public class BlockedContactsActivity extends PassphraseRequiredActivity {
|
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPreCreate() {
|
|
||||||
dynamicTheme.onCreate(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle bundle, boolean ready) {
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
getSupportActionBar().setTitle(R.string.BlockedContactsActivity_blocked_contacts);
|
|
||||||
initFragment(android.R.id.content, new BlockedContactsFragment());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
dynamicTheme.onResume(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSupportNavigateUp() {
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class BlockedContactsFragment
|
|
||||||
extends ListFragment
|
|
||||||
implements LoaderManager.LoaderCallbacks<Cursor>, ListView.OnItemClickListener
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
|
||||||
return inflater.inflate(R.layout.blocked_contacts_fragment, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle bundle) {
|
|
||||||
super.onCreate(bundle);
|
|
||||||
setListAdapter(new BlockedContactAdapter(requireActivity(), GlideApp.with(this), null));
|
|
||||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityCreated(Bundle bundle) {
|
|
||||||
super.onActivityCreated(bundle);
|
|
||||||
getListView().setOnItemClickListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
|
||||||
return new BlockedContactsLoader(getActivity());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
|
||||||
if (getListAdapter() != null) {
|
|
||||||
((CursorAdapter) getListAdapter()).changeCursor(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
if (getListAdapter() != null) {
|
|
||||||
((CursorAdapter) getListAdapter()).changeCursor(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
|
||||||
Recipient recipient = ((BlockedContactListItem)view).getRecipient();
|
|
||||||
BlockUnblockDialog.showUnblockFor(requireContext(), getLifecycle(), recipient, () -> {
|
|
||||||
RecipientUtil.unblock(requireContext(), recipient);
|
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class BlockedContactAdapter extends CursorAdapter {
|
|
||||||
|
|
||||||
private final GlideRequests glideRequests;
|
|
||||||
|
|
||||||
BlockedContactAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, @Nullable Cursor c) {
|
|
||||||
super(context, c);
|
|
||||||
this.glideRequests = glideRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
|
||||||
return LayoutInflater.from(context)
|
|
||||||
.inflate(R.layout.blocked_contact_list_item, parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
|
||||||
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID)));
|
|
||||||
LiveRecipient recipient = Recipient.live(recipientId);
|
|
||||||
|
|
||||||
((BlockedContactListItem) view).set(glideRequests, recipient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,7 @@ import androidx.appcompat.app.AlertDialog;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
@@ -113,8 +114,8 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
||||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||||
|
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||||
@@ -137,8 +138,8 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
|||||||
|
|
||||||
private void processIncomingMessageRecord(MessageRecord messageRecord) {
|
private void processIncomingMessageRecord(MessageRecord messageRecord) {
|
||||||
try {
|
try {
|
||||||
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
||||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||||
|
|
||||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||||
mismatch.getRecipientId(getContext()),
|
mismatch.getRecipientId(getContext()),
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import android.Manifest;
|
|||||||
import android.animation.LayoutTransition;
|
import android.animation.LayoutTransition;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -29,7 +30,6 @@ 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.HorizontalScrollView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -56,6 +56,7 @@ 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.components.emoji.WarningTextView;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactChip;
|
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;
|
||||||
@@ -63,6 +64,8 @@ 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.contacts.sync.DirectoryHelper;
|
||||||
|
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||||
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.mms.GlideRequests;
|
||||||
@@ -70,7 +73,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
|||||||
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.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;
|
||||||
@@ -83,7 +85,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,11 +105,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||||
|
|
||||||
public static final String DISPLAY_MODE = "display_mode";
|
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 REFRESHABLE = "refreshable";
|
||||||
public static final String RECENTS = "recents";
|
public static final String RECENTS = "recents";
|
||||||
public static final String TOTAL_CAPACITY = "total_capacity";
|
public static final String SELECTION_LIMITS = "selection_limits";
|
||||||
public static final String CURRENT_SELECTION = "current_selection";
|
public static final String CURRENT_SELECTION = "current_selection";
|
||||||
|
public static final String HIDE_COUNT = "hide_count";
|
||||||
|
|
||||||
private ConstraintLayout constraintLayout;
|
private ConstraintLayout constraintLayout;
|
||||||
private TextView emptyText;
|
private TextView emptyText;
|
||||||
@@ -124,15 +125,17 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||||
private ChipGroup chipGroup;
|
private ChipGroup chipGroup;
|
||||||
private HorizontalScrollView chipGroupScrollContainer;
|
private HorizontalScrollView chipGroupScrollContainer;
|
||||||
private TextView groupLimit;
|
private WarningTextView groupLimit;
|
||||||
|
|
||||||
@Nullable private FixedViewsAdapter headerAdapter;
|
@Nullable private FixedViewsAdapter headerAdapter;
|
||||||
@Nullable private FixedViewsAdapter footerAdapter;
|
@Nullable private FixedViewsAdapter footerAdapter;
|
||||||
@Nullable private ListCallback listCallback;
|
@Nullable private ListCallback listCallback;
|
||||||
@Nullable private ScrollCallback scrollCallback;
|
@Nullable private ScrollCallback scrollCallback;
|
||||||
private GlideRequests glideRequests;
|
private GlideRequests glideRequests;
|
||||||
private int selectionLimit;
|
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||||
private Set<RecipientId> currentSelection;
|
private Set<RecipientId> currentSelection;
|
||||||
|
private boolean isMulti;
|
||||||
|
private boolean hideCount;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
@@ -207,9 +210,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
Intent intent = requireActivity().getIntent();
|
||||||
|
|
||||||
|
swipeRefresh.setEnabled(intent.getBooleanExtra(REFRESHABLE, true));
|
||||||
|
|
||||||
|
hideCount = intent.getBooleanExtra(HIDE_COUNT, false);
|
||||||
|
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
||||||
|
isMulti = selectionLimit != null;
|
||||||
|
|
||||||
|
if (!isMulti) {
|
||||||
|
selectionLimit = SelectionLimits.NO_LIMITS;
|
||||||
|
}
|
||||||
|
|
||||||
selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
|
|
||||||
currentSelection = getCurrentSelection();
|
currentSelection = getCurrentSelection();
|
||||||
|
|
||||||
updateGroupLimit(getChipCount());
|
updateGroupLimit(getChipCount());
|
||||||
@@ -218,12 +230,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateGroupLimit(int chipCount) {
|
private void updateGroupLimit(int chipCount) {
|
||||||
if (selectionLimit != NO_LIMIT) {
|
int members = currentSelection.size() + chipCount;
|
||||||
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
|
groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
|
||||||
groupLimit.setVisibility(View.VISIBLE);
|
groupLimit.setVisibility(isMulti && !hideCount ? View.VISIBLE : View.GONE);
|
||||||
} else {
|
groupLimit.setWarning(selectionWarningLimitExceeded());
|
||||||
groupLimit.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -255,7 +265,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMulti() {
|
public boolean isMulti() {
|
||||||
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
return isMulti;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeCursor() {
|
private void initializeCursor() {
|
||||||
@@ -265,7 +275,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
glideRequests,
|
glideRequests,
|
||||||
null,
|
null,
|
||||||
new ListClickListener(),
|
new ListClickListener(),
|
||||||
isMulti(),
|
isMulti,
|
||||||
currentSelection);
|
currentSelection);
|
||||||
|
|
||||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||||
@@ -451,15 +461,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
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() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||||
if (selectionLimitReached()) {
|
if (selectionHardLimitReached()) {
|
||||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
|
GroupLimitDialog.showHardLimitMessage(requireContext());
|
||||||
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +484,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
if (onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null)) {
|
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
|
||||||
markContactSelected(selected);
|
markContactSelected(selected);
|
||||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||||
}
|
}
|
||||||
@@ -487,13 +496,13 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
new AlertDialog.Builder(requireContext())
|
new AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
||||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
||||||
.setPositiveButton(R.string.ContactSelectionListFragment_okay, (dialog, which) -> dialog.dismiss())
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
if (onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||||
markContactSelected(selectedContact);
|
markContactSelected(selectedContact);
|
||||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||||
}
|
}
|
||||||
@@ -509,16 +518,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean selectionLimitReached() {
|
private boolean selectionHardLimitReached() {
|
||||||
return getChipCount() >= selectionLimit;
|
return getChipCount() + currentSelection.size() >= selectionLimit.getHardLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean selectionWarningLimitReachedExactly() {
|
||||||
|
return getChipCount() + currentSelection.size() == selectionLimit.getRecommendedLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean selectionWarningLimitExceeded() {
|
||||||
|
return getChipCount() + currentSelection.size() > selectionLimit.getRecommendedLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||||
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
||||||
if (isMulti()) {
|
if (isMulti) {
|
||||||
addChipForSelectedContact(selectedContact);
|
addChipForSelectedContact(selectedContact);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,6 +607,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
private void addChip(@NonNull ContactChip chip) {
|
private void addChip(@NonNull ContactChip chip) {
|
||||||
chipGroup.addView(chip);
|
chipGroup.addView(chip);
|
||||||
updateGroupLimit(getChipCount());
|
updateGroupLimit(getChipCount());
|
||||||
|
if (selectionWarningLimitReachedExactly()) {
|
||||||
|
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getChipCount() {
|
private int getChipCount() {
|
||||||
@@ -632,7 +653,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
|
|
||||||
public interface OnContactSelectedListener {
|
public interface OnContactSelectedListener {
|
||||||
/** @return True if the contact is allowed to be selected, otherwise false. */
|
/** @return True if the contact is allowed to be selected, otherwise false. */
|
||||||
boolean onContactSelected(Optional<RecipientId> recipientId, String number);
|
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
|
||||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public final class GroupMembersDialog {
|
|||||||
public void display() {
|
public void display() {
|
||||||
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
|
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
|
||||||
.setTitle(R.string.ConversationActivity_group_members)
|
.setTitle(R.string.ConversationActivity_group_members)
|
||||||
.setIconAttribute(R.attr.group_members_dialog_icon)
|
.setIcon(R.drawable.ic_group_24)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setView(R.layout.dialog_group_members)
|
.setView(R.layout.dialog_group_members)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChange
|
|||||||
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.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
@@ -40,7 +41,6 @@ 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 PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||||
@@ -63,7 +63,8 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_SMS);
|
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_SMS);
|
||||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
|
||||||
|
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||||
|
|
||||||
setContentView(R.layout.invite_activity);
|
setContentView(R.layout.invite_activity);
|
||||||
@@ -103,7 +104,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
|||||||
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||||
|
|
||||||
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||||
updateSmsButtonText();
|
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||||
|
|
||||||
contactsFragment.setOnContactSelectedListener(this);
|
contactsFragment.setOnContactSelectedListener(this);
|
||||||
shareButton.setOnClickListener(new ShareClickListener());
|
shareButton.setOnClickListener(new ShareClickListener());
|
||||||
@@ -121,14 +122,14 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||||
updateSmsButtonText();
|
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||||
updateSmsButtonText();
|
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendSmsInvites() {
|
private void sendSmsInvites() {
|
||||||
@@ -138,12 +139,11 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
|||||||
.toArray(new SelectedContact[0]));
|
.toArray(new SelectedContact[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSmsButtonText() {
|
private void updateSmsButtonText(int count) {
|
||||||
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,
|
||||||
selectedContacts.size(),
|
count,
|
||||||
selectedContacts.size()));
|
count));
|
||||||
smsSendButton.setEnabled(!selectedContacts.isEmpty());
|
smsSendButton.setEnabled(count > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void onBackPressed() {
|
@Override public void onBackPressed() {
|
||||||
@@ -157,17 +157,17 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
|||||||
private void cancelSmsSelection() {
|
private void cancelSmsSelection() {
|
||||||
setPrimaryColorsToolbarNormal();
|
setPrimaryColorsToolbarNormal();
|
||||||
contactsFragment.reset();
|
contactsFragment.reset();
|
||||||
updateSmsButtonText();
|
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPrimaryColorsToolbarNormal() {
|
private void setPrimaryColorsToolbarNormal() {
|
||||||
primaryToolbar.setBackgroundColor(0);
|
primaryToolbar.setBackgroundColor(0);
|
||||||
primaryToolbar.getNavigationIcon().setColorFilter(null);
|
primaryToolbar.getNavigationIcon().setColorFilter(null);
|
||||||
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.title_text_color_primary));
|
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_primary));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
getWindow().setStatusBarColor(ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
|
WindowUtil.setStatusBarColor(getWindow(), ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
|
||||||
getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor));
|
getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor));
|
||||||
WindowUtil.setLightStatusBarFromTheme(this);
|
WindowUtil.setLightStatusBarFromTheme(this);
|
||||||
}
|
}
|
||||||
@@ -177,11 +177,11 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
|||||||
|
|
||||||
private void setPrimaryColorsToolbarForSms() {
|
private void setPrimaryColorsToolbarForSms() {
|
||||||
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
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(ContextCompat.getColor(this, R.color.signal_text_toolbar_subtitle), PorterDuff.Mode.SRC_IN);
|
||||||
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_toolbar_title));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
WindowUtil.setStatusBarColor(getWindow(), ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||||
WindowUtil.clearLightStatusBar(getWindow());
|
WindowUtil.clearLightStatusBar(getWindow());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in our {@link BuildConfig} to tie together the various attributes of a KBS instance. This
|
||||||
|
* is sitting in the root directory so it can be accessed by the build config.
|
||||||
|
*/
|
||||||
|
public final class KbsEnclave {
|
||||||
|
|
||||||
|
private final String enclaveName;
|
||||||
|
private final String serviceId;
|
||||||
|
private final String mrEnclave;
|
||||||
|
|
||||||
|
public KbsEnclave(@NonNull String enclaveName, @NonNull String serviceId, @NonNull String mrEnclave) {
|
||||||
|
this.enclaveName = enclaveName;
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.mrEnclave = mrEnclave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull String getMrEnclave() {
|
||||||
|
return mrEnclave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull String getEnclaveName() {
|
||||||
|
return enclaveName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull String getServiceId() {
|
||||||
|
return serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
KbsEnclave enclave = (KbsEnclave) o;
|
||||||
|
return enclaveName.equals(enclave.enclaveName) &&
|
||||||
|
serviceId.equals(enclave.serviceId) &&
|
||||||
|
mrEnclave.equals(enclave.mrEnclave);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(enclaveName, serviceId, mrEnclave);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.tracing.Trace;
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
|
||||||
|
@Trace
|
||||||
public class MainActivity extends PassphraseRequiredActivity {
|
public class MainActivity extends PassphraseRequiredActivity {
|
||||||
|
|
||||||
|
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||||
private final MainNavigator navigator = new MainNavigator(this);
|
private final MainNavigator navigator = new MainNavigator(this);
|
||||||
|
|
||||||
@@ -18,6 +27,14 @@ public class MainActivity extends PassphraseRequiredActivity {
|
|||||||
setContentView(R.layout.main_activity);
|
setContentView(R.layout.main_activity);
|
||||||
|
|
||||||
navigator.onCreate(savedInstanceState);
|
navigator.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
handleGroupLinkInIntent(getIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
handleGroupLinkInIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -39,7 +56,22 @@ public class MainActivity extends PassphraseRequiredActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == MainNavigator.REQUEST_CONFIG_CHANGES && resultCode == RESULT_CONFIG_CHANGED) {
|
||||||
|
recreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public @NonNull MainNavigator getNavigator() {
|
public @NonNull MainNavigator getNavigator() {
|
||||||
return navigator;
|
return navigator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleGroupLinkInIntent(Intent intent) {
|
||||||
|
Uri data = intent.getData();
|
||||||
|
if (data != null) {
|
||||||
|
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||||||
|
|
||||||
public class MainNavigator {
|
public class MainNavigator {
|
||||||
|
|
||||||
|
public static final int REQUEST_CONFIG_CHANGES = 901;
|
||||||
|
|
||||||
private final MainActivity activity;
|
private final MainActivity activity;
|
||||||
|
|
||||||
public MainNavigator(@NonNull MainActivity activity) {
|
public MainNavigator(@NonNull MainActivity activity) {
|
||||||
@@ -65,10 +67,9 @@ public class MainNavigator {
|
|||||||
|
|
||||||
public void goToAppSettings() {
|
public void goToAppSettings() {
|
||||||
Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
|
Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
|
||||||
activity.startActivity(intent);
|
activity.startActivityForResult(intent, REQUEST_CONFIG_CHANGES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void goToArchiveList() {
|
public void goToArchiveList() {
|
||||||
getFragmentManager().beginTransaction()
|
getFragmentManager().beginTransaction()
|
||||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
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.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@@ -31,14 +33,14 @@ import android.view.MenuInflater;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
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.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.core.app.ShareCompat;
|
||||||
import androidx.core.util.Pair;
|
import androidx.core.util.Pair;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
@@ -63,14 +65,17 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
|
|||||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
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.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
|
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -119,6 +124,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
private boolean cameFromAllMedia;
|
private boolean cameFromAllMedia;
|
||||||
private boolean showThread;
|
private boolean showThread;
|
||||||
private MediaDatabase.Sorting sorting;
|
private MediaDatabase.Sorting sorting;
|
||||||
|
private FullscreenHelper fullscreenHelper;
|
||||||
|
|
||||||
private @Nullable Cursor cursor = null;
|
private @Nullable Cursor cursor = null;
|
||||||
|
|
||||||
@@ -133,10 +139,16 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
|
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
|
||||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
|
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
|
||||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||||
intent.setDataAndType(attachment.getDataUri(), mediaRecord.getContentType());
|
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attachBaseContext(@NonNull Context newBase) {
|
||||||
|
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||||
|
super.attachBaseContext(newBase);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle bundle, boolean ready) {
|
protected void onCreate(Bundle bundle, boolean ready) {
|
||||||
@@ -147,10 +159,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
|
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
|
||||||
|
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
fullscreenHelper = new FullscreenHelper(this);
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
||||||
|
|
||||||
showSystemUI();
|
|
||||||
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
|
||||||
@@ -196,7 +205,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
if (threadRecipient != null) {
|
if (threadRecipient != null) {
|
||||||
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
||||||
if (threadRecipient.isLocalNumber()) {
|
if (threadRecipient.isSelf()) {
|
||||||
from = getString(R.string.note_to_self);
|
from = getString(R.string.note_to_self);
|
||||||
} else {
|
} else {
|
||||||
to = threadRecipient.getDisplayName(this);
|
to = threadRecipient.getDisplayName(this);
|
||||||
@@ -261,6 +270,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
albumRail = findViewById(R.id.media_preview_album_rail);
|
albumRail = findViewById(R.id.media_preview_album_rail);
|
||||||
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
||||||
|
|
||||||
|
albumRail.setItemAnimator(null); // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
|
||||||
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||||
albumRail.setAdapter(albumRailAdapter);
|
albumRail.setAdapter(albumRailAdapter);
|
||||||
|
|
||||||
@@ -273,9 +283,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
anchorMarginsToBottomInsets(detailsContainer);
|
anchorMarginsToBottomInsets(detailsContainer);
|
||||||
|
|
||||||
anchorMarginsToTopInsets(toolbarLayout);
|
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||||
|
|
||||||
showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeResources() {
|
private void initializeResources() {
|
||||||
@@ -379,6 +389,27 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void share() {
|
||||||
|
MediaItem mediaItem = getCurrentMediaItem();
|
||||||
|
|
||||||
|
if (mediaItem != null) {
|
||||||
|
Uri publicUri = PartAuthority.getAttachmentPublicUri(mediaItem.uri);
|
||||||
|
String mimeType = Intent.normalizeMimeType(mediaItem.type);
|
||||||
|
Intent shareIntent = ShareCompat.IntentBuilder.from(this)
|
||||||
|
.setStream(publicUri)
|
||||||
|
.setType(mimeType)
|
||||||
|
.createChooserIntent()
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(shareIntent);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.w(TAG, "No activity existed to share the media.", e);
|
||||||
|
Toast.makeText(this, R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("CodeBlock2Expr")
|
@SuppressWarnings("CodeBlock2Expr")
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private void saveToDisk() {
|
private void saveToDisk() {
|
||||||
@@ -386,21 +417,30 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
if (mediaItem != null) {
|
if (mediaItem != null) {
|
||||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||||
|
if (StorageUtil.canWriteToMediaStore()) {
|
||||||
|
performSavetoDisk(mediaItem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
performSavetoDisk(mediaItem);
|
||||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
|
||||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void performSavetoDisk(@NonNull MediaItem mediaItem) {
|
||||||
|
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||||
|
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||||
|
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private void deleteMedia() {
|
private void deleteMedia() {
|
||||||
MediaItem mediaItem = getCurrentMediaItem();
|
MediaItem mediaItem = getCurrentMediaItem();
|
||||||
@@ -409,7 +449,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
builder.setIcon(R.drawable.ic_warning);
|
||||||
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
|
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
|
||||||
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
|
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
|
||||||
builder.setCancelable(true);
|
builder.setCancelable(true);
|
||||||
@@ -431,36 +471,45 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
super.onPrepareOptionsMenu(menu);
|
|
||||||
|
|
||||||
menu.clear();
|
menu.clear();
|
||||||
MenuInflater inflater = this.getMenuInflater();
|
MenuInflater inflater = this.getMenuInflater();
|
||||||
inflater.inflate(R.menu.media_preview, menu);
|
inflater.inflate(R.menu.media_preview, menu);
|
||||||
|
|
||||||
|
super.onCreateOptionsMenu(menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||||
if (!isMediaInDb()) {
|
if (!isMediaInDb()) {
|
||||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||||
menu.findItem(R.id.delete).setVisible(false);
|
menu.findItem(R.id.delete).setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
|
||||||
|
menu.findItem(R.id.media_preview__share).setVisible(Build.VERSION.SDK_INT >= 26);
|
||||||
|
|
||||||
if (cameFromAllMedia) {
|
if (cameFromAllMedia) {
|
||||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super.onPrepareOptionsMenu(menu);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
super.onOptionsItemSelected(item);
|
super.onOptionsItemSelected(item);
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
int itemId = item.getItemId();
|
||||||
case R.id.media_preview__overview: showOverview(); return true;
|
|
||||||
case R.id.media_preview__forward: forward(); return true;
|
if (itemId == R.id.media_preview__overview) { showOverview(); return true; }
|
||||||
case R.id.save: saveToDisk(); return true;
|
if (itemId == R.id.media_preview__forward) { forward(); return true; }
|
||||||
case R.id.delete: deleteMedia(); return true;
|
if (itemId == R.id.media_preview__share) { share(); return true; }
|
||||||
case android.R.id.home: finish(); return true;
|
if (itemId == R.id.save) { saveToDisk(); return true; }
|
||||||
}
|
if (itemId == R.id.delete) { deleteMedia(); return true; }
|
||||||
|
if (itemId == android.R.id.home) { finish(); return true; }
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -541,7 +590,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean singleTapOnMedia() {
|
public boolean singleTapOnMedia() {
|
||||||
toggleUiVisibility();
|
fullscreenHelper.toggleUiVisibility();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,32 +600,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 );
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ViewPagerListener extends ExtendedOnPageChangedListener {
|
private class ViewPagerListener extends ExtendedOnPageChangedListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -692,33 +715,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void anchorMarginsToTopInsets(@NonNull View viewToAnchor) {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor, (view, insets) -> {
|
|
||||||
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
|
|
||||||
|
|
||||||
layoutParams.setMargins(insets.getSystemWindowInsetLeft(),
|
|
||||||
insets.getSystemWindowInsetTop(),
|
|
||||||
insets.getSystemWindowInsetRight(),
|
|
||||||
layoutParams.bottomMargin);
|
|
||||||
|
|
||||||
view.setLayoutParams(layoutParams);
|
|
||||||
|
|
||||||
return insets;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
|
private static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
|
||||||
|
|
||||||
@SuppressLint("UseSparseArrays")
|
@SuppressLint("UseSparseArrays")
|
||||||
@@ -796,7 +792,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
return new MediaItem(Recipient.live(recipientId).get(),
|
return new MediaItem(Recipient.live(recipientId).get(),
|
||||||
Recipient.live(threadRecipientId).get(),
|
Recipient.live(threadRecipientId).get(),
|
||||||
attachment,
|
attachment,
|
||||||
Objects.requireNonNull(attachment.getDataUri()),
|
Objects.requireNonNull(attachment.getUri()),
|
||||||
mediaRecord.getContentType(),
|
mediaRecord.getContentType(),
|
||||||
mediaRecord.getDate(),
|
mediaRecord.getDate(),
|
||||||
mediaRecord.isOutgoing());
|
mediaRecord.isOutgoing());
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ 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.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
@@ -60,21 +61,22 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||||
if (recipientId.isPresent()) {
|
if (recipientId.isPresent()) {
|
||||||
launch(Recipient.resolved(recipientId.get()));
|
launch(Recipient.resolved(recipientId.get()));
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||||
if (FeatureFlags.cds() && NetworkConstraint.isMet(this)) {
|
|
||||||
Log.i(TAG, "[onContactSelected] CDS enabled. Doing contact refresh.");
|
if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
|
||||||
|
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
|
||||||
|
|
||||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||||
|
|
||||||
SimpleTask.run(getLifecycle(), () -> {
|
SimpleTask.run(getLifecycle(), () -> {
|
||||||
Recipient resolved = Recipient.external(this, number);
|
Recipient resolved = Recipient.external(this, number);
|
||||||
|
|
||||||
if (!resolved.isRegistered()) {
|
if (!resolved.isRegistered() || !resolved.hasUuid()) {
|
||||||
Log.i(TAG, "[onContactSelected] Not registered. Doing a directory refresh.");
|
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||||
try {
|
try {
|
||||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||||
resolved = Recipient.resolved(resolved.getId());
|
resolved = Recipient.resolved(resolved.getId());
|
||||||
@@ -98,11 +100,11 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
|
|
||||||
private void launch(Recipient recipient) {
|
private void launch(Recipient recipient) {
|
||||||
Intent intent = new Intent(this, ConversationActivity.class);
|
Intent intent = new Intent(this, ConversationActivity.class);
|
||||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
|
||||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
|
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
|
||||||
intent.setDataAndType(getIntent().getData(), getIntent().getType());
|
intent.setDataAndType(getIntent().getData(), getIntent().getType());
|
||||||
|
|
||||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
|
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||||
|
|
||||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
|
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
|
||||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||||
@@ -138,11 +140,11 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
menu.clear();
|
menu.clear();
|
||||||
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
|
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
|
||||||
|
|
||||||
super.onPrepareOptionsMenu(menu);
|
super.onCreateOptionsMenu(menu);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,12 +66,6 @@ public class PassphraseCreateActivity extends PassphraseActivity {
|
|||||||
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
|
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
|
||||||
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
|
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
|
||||||
|
|
||||||
TextSecurePreferences.setLastExperienceVersionCode(PassphraseCreateActivity.this, Util.getCanonicalVersionCode());
|
|
||||||
TextSecurePreferences.setPasswordDisabled(PassphraseCreateActivity.this, true);
|
|
||||||
TextSecurePreferences.setReadReceiptsEnabled(PassphraseCreateActivity.this, true);
|
|
||||||
TextSecurePreferences.setTypingIndicatorsEnabled(PassphraseCreateActivity.this, true);
|
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(PassphraseCreateActivity.this, false);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,12 +132,13 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
MenuInflater inflater = this.getMenuInflater();
|
MenuInflater inflater = this.getMenuInflater();
|
||||||
menu.clear();
|
menu.clear();
|
||||||
|
|
||||||
inflater.inflate(R.menu.log_submit, menu);
|
inflater.inflate(R.menu.log_submit, menu);
|
||||||
super.onPrepareOptionsMenu(menu);
|
|
||||||
|
super.onCreateOptionsMenu(menu);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle icicle, boolean ready) {
|
protected void onCreate(Bundle icicle, boolean ready) {
|
||||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
|
||||||
super.onCreate(icicle, ready);
|
super.onCreate(icicle, ready);
|
||||||
|
|
||||||
initializeToolbar();
|
initializeToolbar();
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ public class SmsSendtoActivity extends Activity {
|
|||||||
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
||||||
} else {
|
} else {
|
||||||
Recipient recipient = Recipient.external(this, destination.getDestination());
|
Recipient recipient = Recipient.external(this, destination.getDestination());
|
||||||
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
|
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||||
|
|
||||||
nextIntent = new Intent(this, ConversationActivity.class);
|
nextIntent = new Intent(this, ConversationActivity.class);
|
||||||
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
|
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
|
||||||
nextIntent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
nextIntent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||||
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
|
||||||
}
|
}
|
||||||
return nextIntent;
|
return nextIntent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import android.animation.ValueAnimator;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
@@ -56,6 +57,7 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.widget.SwitchCompat;
|
import androidx.appcompat.widget.SwitchCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
@@ -85,16 +87,19 @@ import org.thoughtcrime.securesms.util.IdentityUtil;
|
|||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
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.WindowUtil;
|
||||||
import org.whispersystems.libsignal.IdentityKey;
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||||
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
|
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
|
||||||
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
|
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
|
||||||
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||||
|
|
||||||
@@ -224,9 +229,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
|||||||
private void setActionBarNotificationBarColor(MaterialColor color) {
|
private void setActionBarNotificationBarColor(MaterialColor color) {
|
||||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
|
||||||
getWindow().setStatusBarColor(color.toStatusBarColor(this));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||||
@@ -307,16 +310,26 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
|||||||
byte[] localId;
|
byte[] localId;
|
||||||
byte[] remoteId;
|
byte[] remoteId;
|
||||||
|
|
||||||
if (FeatureFlags.cds() && recipient.resolve().getUuid().isPresent()) {
|
Recipient resolved = recipient.resolve();
|
||||||
|
|
||||||
|
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
|
||||||
Log.i(TAG, "Using UUID (version 2).");
|
Log.i(TAG, "Using UUID (version 2).");
|
||||||
version = 2;
|
version = 2;
|
||||||
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
|
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
|
||||||
remoteId = UuidUtil.toByteArray(recipient.resolve().getUuid().get());
|
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
|
||||||
} else {
|
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
|
||||||
Log.i(TAG, "Using E164 (version 1).");
|
Log.i(TAG, "Using E164 (version 1).");
|
||||||
version = 1;
|
version = 1;
|
||||||
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
|
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
|
||||||
remoteId = recipient.resolve().requireE164().getBytes();
|
remoteId = resolved.requireE164().getBytes();
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getUuid().isPresent(), resolved.getE164().isPresent()));
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
|
||||||
|
.setOnDismissListener(dialog -> requireActivity().finish())
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recipient.observe(this, this::setRecipientText);
|
this.recipient.observe(this, this::setRecipientText);
|
||||||
|
|||||||
@@ -19,17 +19,12 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.PictureInPictureParams;
|
import android.app.PictureInPictureParams;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.DialogInterface.OnClickListener;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
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.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.util.Rational;
|
import android.util.Rational;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
@@ -37,7 +32,6 @@ import android.view.WindowManager;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.AppCompatTextView;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
@@ -45,10 +39,12 @@ 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.TooltipPopup;
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
|
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||||
@@ -56,24 +52,19 @@ 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.sms.MessageSender;
|
||||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
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.whispersystems.libsignal.IdentityKey;
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
|
||||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
|
|
||||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
|
||||||
|
|
||||||
public class WebRtcCallActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
|
|
||||||
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
|
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
|
||||||
|
|
||||||
private static final int STANDARD_DELAY_FINISH = 1000;
|
private static final int STANDARD_DELAY_FINISH = 1000;
|
||||||
public static final int BUSY_SIGNAL_DELAY_FINISH = 5500;
|
|
||||||
|
|
||||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||||
@@ -95,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
setContentView(R.layout.webrtc_call_activity);
|
setContentView(R.layout.webrtc_call_activity);
|
||||||
|
//noinspection ConstantConditions
|
||||||
getSupportActionBar().hide();
|
getSupportActionBar().hide();
|
||||||
|
|
||||||
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||||
@@ -134,6 +126,11 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
if (!isInPipMode()) {
|
if (!isInPipMode()) {
|
||||||
EventBus.getDefault().unregister(this);
|
EventBus.getDefault().unregister(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||||
|
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -142,11 +139,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
super.onStop();
|
super.onStop();
|
||||||
|
|
||||||
EventBus.getDefault().unregister(this);
|
EventBus.getDefault().unregister(this);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||||
super.onConfigurationChanged(newConfiguration);
|
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||||
|
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
|
||||||
|
startService(intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -172,7 +171,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean enterPipModeIfPossible() {
|
private boolean enterPipModeIfPossible() {
|
||||||
if (isSystemPipEnabledAndAvailable()) {
|
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
|
||||||
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||||
.setAspectRatio(new Rational(9, 16))
|
.setAspectRatio(new Rational(9, 16))
|
||||||
.build();
|
.build();
|
||||||
@@ -206,21 +205,29 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeResources() {
|
private void initializeResources() {
|
||||||
callScreen = ViewUtil.findById(this, R.id.callScreen);
|
callScreen = findViewById(R.id.callScreen);
|
||||||
callScreen.setControlsListener(new ControlsListener());
|
callScreen.setControlsListener(new ControlsListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeViewModel() {
|
private void initializeViewModel() {
|
||||||
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
|
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
|
||||||
viewModel.setIsInPipMode(isInPipMode());
|
viewModel.setIsInPipMode(isInPipMode());
|
||||||
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
|
|
||||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
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.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||||
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
|
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
|
||||||
|
|
||||||
|
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||||
|
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||||
|
if (state != null) {
|
||||||
|
if (state.needsNewRequestSizes()) {
|
||||||
|
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||||
|
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
|
||||||
|
startService(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
||||||
@@ -385,19 +392,17 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
startService(intent);
|
startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
|
|
||||||
callScreen.setRecipient(event.getRecipient());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
||||||
callScreen.setRecipient(event.getRecipient());
|
if (event.getGroupState().isNotIdle()) {
|
||||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
|
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||||
|
} else {
|
||||||
|
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
|
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
|
||||||
Log.i(TAG, "handleTerminate called: " + hangupType.name());
|
Log.i(TAG, "handleTerminate called: " + hangupType.name());
|
||||||
|
|
||||||
callScreen.setRecipient(recipient);
|
|
||||||
callScreen.setStatusFromHangupType(hangupType);
|
callScreen.setStatusFromHangupType(hangupType);
|
||||||
|
|
||||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||||
@@ -408,99 +413,74 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
delayedFinish();
|
delayedFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCallRinging(@NonNull WebRtcViewModel event) {
|
private void handleCallRinging() {
|
||||||
callScreen.setRecipient(event.getRecipient());
|
|
||||||
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCallBusy(@NonNull WebRtcViewModel event) {
|
private void handleCallBusy() {
|
||||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||||
callScreen.setRecipient(event.getRecipient());
|
|
||||||
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
||||||
|
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
|
||||||
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.setRecipient(event.getRecipient());
|
if (event.getGroupState().isNotIdleOrConnected()) {
|
||||||
|
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
|
private void handleRecipientUnavailable() {
|
||||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||||
callScreen.setRecipient(event.getRecipient());
|
|
||||||
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
|
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
|
||||||
delayedFinish();
|
delayedFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleServerFailure(@NonNull WebRtcViewModel event) {
|
private void handleServerFailure() {
|
||||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||||
callScreen.setRecipient(event.getRecipient());
|
|
||||||
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
|
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
|
||||||
delayedFinish();
|
delayedFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
|
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
|
||||||
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
|
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
|
||||||
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
|
new AlertDialog.Builder(this)
|
||||||
dialog.setTitle(R.string.RedPhone_number_not_registered);
|
.setTitle(R.string.RedPhone_number_not_registered)
|
||||||
dialog.setIconAttribute(R.attr.dialog_alert_icon);
|
.setIcon(R.drawable.ic_warning)
|
||||||
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice);
|
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
|
||||||
dialog.setCancelable(true);
|
.setCancelable(true)
|
||||||
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
|
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||||
@Override
|
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
.show();
|
||||||
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
|
||||||
@Override
|
|
||||||
public void onCancel(DialogInterface dialog) {
|
|
||||||
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialog.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
||||||
final IdentityKey theirKey = event.getIdentityKey();
|
final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
|
||||||
final Recipient recipient = event.getRecipient();
|
final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
|
||||||
|
|
||||||
if (theirKey == null) {
|
if (theirKey == null) {
|
||||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
String name = recipient.getDisplayName(this);
|
SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
|
||||||
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name);
|
}
|
||||||
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
|
|
||||||
|
|
||||||
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
@Override
|
||||||
|
public void onSendAnywayAfterSafetyNumberChange() {
|
||||||
|
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||||
|
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||||
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
|
||||||
|
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.AUDIO_CALL.getCode());
|
||||||
|
|
||||||
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this);
|
startService(intent);
|
||||||
untrustedIdentityExplanation.setText(spannableString);
|
}
|
||||||
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
|
|
||||||
new AlertDialog.Builder(this)
|
@Override
|
||||||
.setView(untrustedIdentityExplanation)
|
public void onMessageResentAfterSafetyNumberChange() { }
|
||||||
.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();
|
@Override
|
||||||
|
public void onCanceled() {
|
||||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL);
|
||||||
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 isSystemPipEnabledAndAvailable() {
|
private boolean isSystemPipEnabledAndAvailable() {
|
||||||
@@ -517,32 +497,30 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||||
public void onEventMainThread(final WebRtcViewModel event) {
|
public void onEventMainThread(@NonNull WebRtcViewModel event) {
|
||||||
Log.i(TAG, "Got message from service: " + event);
|
Log.i(TAG, "Got message from service: " + event);
|
||||||
|
|
||||||
viewModel.setRecipient(event.getRecipient());
|
viewModel.setRecipient(event.getRecipient());
|
||||||
|
callScreen.setRecipient(event.getRecipient());
|
||||||
|
|
||||||
switch (event.getState()) {
|
switch (event.getState()) {
|
||||||
|
case CALL_PRE_JOIN: handleCallPreJoin(event); break;
|
||||||
case CALL_CONNECTED: handleCallConnected(event); break;
|
case CALL_CONNECTED: handleCallConnected(event); break;
|
||||||
case NETWORK_FAILURE: handleServerFailure(event); break;
|
case NETWORK_FAILURE: handleServerFailure(); break;
|
||||||
case CALL_RINGING: handleCallRinging(event); break;
|
case CALL_RINGING: handleCallRinging(); break;
|
||||||
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||||
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||||
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
|
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
|
||||||
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||||
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
|
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
|
||||||
case CALL_INCOMING: handleIncomingCall(event); break;
|
|
||||||
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
||||||
case CALL_BUSY: handleCallBusy(event); break;
|
case CALL_BUSY: handleCallBusy(); break;
|
||||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
callScreen.setLocalRenderer(event.getLocalRenderer());
|
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||||
callScreen.setRemoteRenderer(event.getRemoteRenderer());
|
|
||||||
|
|
||||||
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
|
||||||
|
|
||||||
viewModel.updateFromWebRtcViewModel(event, enableVideo);
|
viewModel.updateFromWebRtcViewModel(event, enableVideo);
|
||||||
|
|
||||||
@@ -552,8 +530,32 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
|
||||||
|
if (event.getGroupState().isNotIdle()) {
|
||||||
|
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class ControlsListener implements WebRtcCallView.ControlsListener {
|
private final class ControlsListener implements WebRtcCallView.ControlsListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartCall(boolean isVideoCall) {
|
||||||
|
enableVideoIfAvailable = isVideoCall;
|
||||||
|
|
||||||
|
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||||
|
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||||
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
|
||||||
|
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
|
||||||
|
startService(intent);
|
||||||
|
|
||||||
|
MessageSender.onMessageSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCancelStartCall() {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onControlsFadeOut() {
|
public void onControlsFadeOut() {
|
||||||
if (videoTooltip != null) {
|
if (videoTooltip != null) {
|
||||||
@@ -618,9 +620,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDownCaretPressed() {
|
public void onShowParticipantsList() {
|
||||||
|
CallParticipantsListDialog.show(getSupportFragmentManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
|
||||||
|
viewModel.setIsViewingFocusedParticipant(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.thoughtcrime.securesms.animation;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.Transformation;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class ResizeAnimation extends Animation {
|
||||||
|
|
||||||
|
private final View target;
|
||||||
|
private final int targetWidthPx;
|
||||||
|
private final int targetHeightPx;
|
||||||
|
|
||||||
|
private int startWidth;
|
||||||
|
private int startHeight;
|
||||||
|
|
||||||
|
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
|
||||||
|
this.target = target;
|
||||||
|
this.targetWidthPx = targetWidthPx;
|
||||||
|
this.targetHeightPx = targetHeightPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||||
|
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
|
||||||
|
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams params = target.getLayoutParams();
|
||||||
|
|
||||||
|
params.width = newWidth;
|
||||||
|
params.height = newHeight;
|
||||||
|
|
||||||
|
target.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(int width, int height, int parentWidth, int parentHeight) {
|
||||||
|
super.initialize(width, height, parentWidth, parentHeight);
|
||||||
|
|
||||||
|
this.startWidth = width;
|
||||||
|
this.startHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean willChangeBounds() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,10 +106,7 @@ public abstract class Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public abstract Uri getDataUri();
|
public abstract Uri getUri();
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public abstract Uri getThumbnailUri();
|
|
||||||
|
|
||||||
public int getTransferState() {
|
public int getTransferState() {
|
||||||
return transferState;
|
return transferState;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class DatabaseAttachment extends Attachment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public Uri getDataUri() {
|
public Uri getUri() {
|
||||||
if (hasData) {
|
if (hasData) {
|
||||||
return PartAuthority.getAttachmentDataUri(attachmentId);
|
return PartAuthority.getAttachmentDataUri(attachmentId);
|
||||||
} else {
|
} else {
|
||||||
@@ -65,16 +65,6 @@ public class DatabaseAttachment extends Attachment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public Uri getThumbnailUri() {
|
|
||||||
if (hasThumbnail) {
|
|
||||||
return PartAuthority.getAttachmentThumbnailUri(attachmentId);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AttachmentId getAttachmentId() {
|
public AttachmentId getAttachmentId() {
|
||||||
return attachmentId;
|
return attachmentId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ public class MmsNotificationAttachment extends Attachment {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Uri getDataUri() {
|
public Uri getUri() {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Uri getThumbnailUri() {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,17 +42,10 @@ public class PointerAttachment extends Attachment {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Uri getDataUri() {
|
public Uri getUri() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Uri getThumbnailUri() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
|
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
|
||||||
List<Attachment> results = new LinkedList<>();
|
List<Attachment> results = new LinkedList<>();
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,7 @@ public class TombstoneAttachment extends Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Uri getDataUri() {
|
public @Nullable Uri getUri() {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable Uri getThumbnailUri() {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
|||||||
public class UriAttachment extends Attachment {
|
public class UriAttachment extends Attachment {
|
||||||
|
|
||||||
private final @NonNull Uri dataUri;
|
private final @NonNull Uri dataUri;
|
||||||
private final @Nullable Uri thumbnailUri;
|
|
||||||
|
|
||||||
public UriAttachment(@NonNull Uri uri,
|
public UriAttachment(@NonNull Uri uri,
|
||||||
@NonNull String contentType,
|
@NonNull String contentType,
|
||||||
@@ -29,11 +28,10 @@ public class UriAttachment extends Attachment {
|
|||||||
@Nullable AudioHash audioHash,
|
@Nullable AudioHash audioHash,
|
||||||
@Nullable TransformProperties transformProperties)
|
@Nullable TransformProperties transformProperties)
|
||||||
{
|
{
|
||||||
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UriAttachment(@NonNull Uri dataUri,
|
public UriAttachment(@NonNull Uri dataUri,
|
||||||
@Nullable Uri thumbnailUri,
|
|
||||||
@NonNull String contentType,
|
@NonNull String contentType,
|
||||||
int transferState,
|
int transferState,
|
||||||
long size,
|
long size,
|
||||||
@@ -51,22 +49,15 @@ public class UriAttachment extends Attachment {
|
|||||||
@Nullable TransformProperties transformProperties)
|
@Nullable TransformProperties transformProperties)
|
||||||
{
|
{
|
||||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||||
this.dataUri = dataUri;
|
this.dataUri = dataUri;
|
||||||
this.thumbnailUri = thumbnailUri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public Uri getDataUri() {
|
public Uri getUri() {
|
||||||
return dataUri;
|
return dataUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public Uri getThumbnailUri() {
|
|
||||||
return thumbnailUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object other) {
|
public boolean equals(Object other) {
|
||||||
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
|
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
|
||||||
|
|||||||
@@ -1,378 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.audio;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.hardware.Sensor;
|
|
||||||
import android.hardware.SensorEvent;
|
|
||||||
import android.hardware.SensorEventListener;
|
|
||||||
import android.hardware.SensorManager;
|
|
||||||
import android.media.AudioManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.os.PowerManager;
|
|
||||||
import android.os.PowerManager.WakeLock;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
|
||||||
import com.google.android.exoplayer2.LoadControl;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
public class AudioSlidePlayer implements SensorEventListener {
|
|
||||||
|
|
||||||
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
|
||||||
|
|
||||||
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
|
|
||||||
|
|
||||||
private final @NonNull Context context;
|
|
||||||
private final @NonNull AudioSlide slide;
|
|
||||||
private final @NonNull Handler progressEventHandler;
|
|
||||||
private final @NonNull AudioManager audioManager;
|
|
||||||
private final @NonNull SensorManager sensorManager;
|
|
||||||
private final @NonNull Sensor proximitySensor;
|
|
||||||
private final @Nullable WakeLock wakeLock;
|
|
||||||
|
|
||||||
private @NonNull WeakReference<Listener> listener;
|
|
||||||
private @Nullable SimpleExoPlayer mediaPlayer;
|
|
||||||
private long startTime;
|
|
||||||
|
|
||||||
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
|
|
||||||
@NonNull AudioSlide slide,
|
|
||||||
@NonNull Listener listener)
|
|
||||||
{
|
|
||||||
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
|
|
||||||
playing.get().setListener(listener);
|
|
||||||
return playing.get();
|
|
||||||
} else {
|
|
||||||
return new AudioSlidePlayer(context, slide, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AudioSlidePlayer(@NonNull Context context,
|
|
||||||
@NonNull AudioSlide slide,
|
|
||||||
@NonNull Listener listener)
|
|
||||||
{
|
|
||||||
this.context = context;
|
|
||||||
this.slide = slide;
|
|
||||||
this.listener = new WeakReference<>(listener);
|
|
||||||
this.progressEventHandler = new ProgressEventHandler(this);
|
|
||||||
this.audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
|
|
||||||
this.sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
|
||||||
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
|
||||||
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
|
|
||||||
} else {
|
|
||||||
this.wakeLock = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void play(final double progress) throws IOException {
|
|
||||||
play(progress, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void play(final double progress, boolean earpiece) throws IOException {
|
|
||||||
if (this.mediaPlayer != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slide.getUri() == null) {
|
|
||||||
throw new IOException("Slide has no URI!");
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
|
|
||||||
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
|
|
||||||
this.startTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
mediaPlayer.prepare(createMediaSource(slide.getUri()));
|
|
||||||
mediaPlayer.setPlayWhenReady(true);
|
|
||||||
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
|
|
||||||
.setContentType(earpiece ? C.CONTENT_TYPE_SPEECH : C.CONTENT_TYPE_MUSIC)
|
|
||||||
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
|
|
||||||
.build());
|
|
||||||
mediaPlayer.addListener(new Player.EventListener() {
|
|
||||||
|
|
||||||
boolean started = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
|
||||||
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
|
|
||||||
switch (playbackState) {
|
|
||||||
case Player.STATE_READY:
|
|
||||||
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
|
|
||||||
synchronized (AudioSlidePlayer.this) {
|
|
||||||
if (mediaPlayer == null) return;
|
|
||||||
|
|
||||||
if (started) {
|
|
||||||
Log.d(TAG, "Already started. Ignoring.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
started = true;
|
|
||||||
|
|
||||||
if (progress > 0) {
|
|
||||||
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
|
||||||
}
|
|
||||||
|
|
||||||
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
|
|
||||||
|
|
||||||
setPlaying(AudioSlidePlayer.this);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyOnStart();
|
|
||||||
progressEventHandler.sendEmptyMessage(0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Player.STATE_ENDED:
|
|
||||||
Log.i(TAG, "onComplete");
|
|
||||||
synchronized (AudioSlidePlayer.this) {
|
|
||||||
mediaPlayer = null;
|
|
||||||
|
|
||||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
|
||||||
|
|
||||||
if (wakeLock != null && wakeLock.isHeld()) {
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
|
||||||
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyOnStop();
|
|
||||||
progressEventHandler.removeMessages(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerError(ExoPlaybackException error) {
|
|
||||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
|
||||||
|
|
||||||
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
synchronized (AudioSlidePlayer.this) {
|
|
||||||
mediaPlayer = null;
|
|
||||||
|
|
||||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
|
||||||
|
|
||||||
if (wakeLock != null && wakeLock.isHeld()) {
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
|
||||||
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyOnStop();
|
|
||||||
progressEventHandler.removeMessages(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private MediaSource createMediaSource(@NonNull Uri uri) {
|
|
||||||
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
|
||||||
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
|
|
||||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
|
|
||||||
|
|
||||||
return new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
|
|
||||||
.setExtractorsFactory(extractorsFactory)
|
|
||||||
.createMediaSource(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void stop() {
|
|
||||||
Log.i(TAG, "Stop called!");
|
|
||||||
|
|
||||||
removePlaying(this);
|
|
||||||
|
|
||||||
if (this.mediaPlayer != null) {
|
|
||||||
this.mediaPlayer.stop();
|
|
||||||
this.mediaPlayer.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
|
||||||
|
|
||||||
this.mediaPlayer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized static void stopAll() {
|
|
||||||
if (playing.isPresent()) {
|
|
||||||
playing.get().stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(@NonNull Listener listener) {
|
|
||||||
this.listener = new WeakReference<>(listener);
|
|
||||||
|
|
||||||
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
|
|
||||||
notifyOnStart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NonNull AudioSlide getAudioSlide() {
|
|
||||||
return slide;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Pair<Double, Integer> getProgress() {
|
|
||||||
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
|
|
||||||
return new Pair<>(0D, 0);
|
|
||||||
} else {
|
|
||||||
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
|
|
||||||
(int) mediaPlayer.getCurrentPosition());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnStart() {
|
|
||||||
Util.runOnMain(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
getListener().onStart();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnStop() {
|
|
||||||
Util.runOnMain(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
getListener().onStop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnProgress(final double progress, final long millis) {
|
|
||||||
Util.runOnMain(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
getListener().onProgress(progress, millis);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NonNull Listener getListener() {
|
|
||||||
Listener listener = this.listener.get();
|
|
||||||
|
|
||||||
if (listener != null) return listener;
|
|
||||||
else return new Listener() {
|
|
||||||
@Override
|
|
||||||
public void onStart() {}
|
|
||||||
@Override
|
|
||||||
public void onStop() {}
|
|
||||||
@Override
|
|
||||||
public void onProgress(double progress, long millis) {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
|
|
||||||
if (playing.isPresent() && playing.get() != player) {
|
|
||||||
playing.get().notifyOnStop();
|
|
||||||
playing.get().stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
playing = Optional.of(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
|
|
||||||
if (playing.isPresent() && playing.get() == player) {
|
|
||||||
playing = Optional.absent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSensorChanged(SensorEvent event) {
|
|
||||||
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) return;
|
|
||||||
if (mediaPlayer == null || mediaPlayer.getPlaybackState() != Player.STATE_READY) return;
|
|
||||||
|
|
||||||
int streamType;
|
|
||||||
|
|
||||||
if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
|
|
||||||
streamType = AudioManager.STREAM_VOICE_CALL;
|
|
||||||
} else {
|
|
||||||
streamType = AudioManager.STREAM_MUSIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamType == AudioManager.STREAM_VOICE_CALL &&
|
|
||||||
mediaPlayer.getAudioStreamType() != streamType &&
|
|
||||||
!audioManager.isWiredHeadsetOn())
|
|
||||||
{
|
|
||||||
double position = mediaPlayer.getCurrentPosition();
|
|
||||||
double duration = mediaPlayer.getDuration();
|
|
||||||
double progress = position / duration;
|
|
||||||
|
|
||||||
if (wakeLock != null) wakeLock.acquire();
|
|
||||||
stop();
|
|
||||||
try {
|
|
||||||
play(progress, true);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
} else if (streamType == AudioManager.STREAM_MUSIC &&
|
|
||||||
mediaPlayer.getAudioStreamType() != streamType &&
|
|
||||||
System.currentTimeMillis() - startTime > 500)
|
|
||||||
{
|
|
||||||
if (wakeLock != null) wakeLock.release();
|
|
||||||
stop();
|
|
||||||
notifyOnStop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Listener {
|
|
||||||
void onStart();
|
|
||||||
void onStop();
|
|
||||||
void onProgress(double progress, long millis);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ProgressEventHandler extends Handler {
|
|
||||||
|
|
||||||
private final WeakReference<AudioSlidePlayer> playerReference;
|
|
||||||
|
|
||||||
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
|
|
||||||
this.playerReference = new WeakReference<>(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
AudioSlidePlayer player = playerReference.get();
|
|
||||||
|
|
||||||
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair<Double, Integer> progress = player.getProgress();
|
|
||||||
player.notifyOnProgress(progress.first, progress.second);
|
|
||||||
sendEmptyMessageDelayed(0, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
|
|
||||||
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,10 @@ package org.thoughtcrime.securesms.backup;
|
|||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
@@ -13,10 +17,14 @@ 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.RequiresApi;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
|
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
|
||||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||||
@@ -26,25 +34,49 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
|||||||
|
|
||||||
public class BackupDialog {
|
public class BackupDialog {
|
||||||
|
|
||||||
public static void showEnableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
private static final String TAG = Log.tag(BackupDialog.class);
|
||||||
|
|
||||||
|
public static void showEnableBackupDialog(@NonNull Context context,
|
||||||
|
@Nullable Intent backupDirectorySelectionIntent,
|
||||||
|
@Nullable String backupDirectoryDisplayName,
|
||||||
|
@NonNull Runnable onBackupsEnabled)
|
||||||
|
{
|
||||||
String[] password = BackupUtil.generateBackupPassphrase();
|
String[] password = BackupUtil.generateBackupPassphrase();
|
||||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
.setTitle(R.string.BackupDialog_enable_local_backups)
|
||||||
.setView(R.layout.backup_enable_dialog)
|
.setView(backupDirectorySelectionIntent != null ? R.layout.backup_enable_dialog_v29 : R.layout.backup_enable_dialog)
|
||||||
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
dialog.setOnShowListener(created -> {
|
dialog.setOnShowListener(created -> {
|
||||||
|
if (backupDirectoryDisplayName != null) {
|
||||||
|
TextView folderName = dialog.findViewById(R.id.backup_enable_dialog_folder_name);
|
||||||
|
if (folderName != null) {
|
||||||
|
folderName.setText(backupDirectoryDisplayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
|
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||||
button.setOnClickListener(v -> {
|
button.setOnClickListener(v -> {
|
||||||
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
||||||
if (confirmationCheckBox.isChecked()) {
|
if (confirmationCheckBox.isChecked()) {
|
||||||
|
if (backupDirectorySelectionIntent != null && backupDirectorySelectionIntent.getData() != null) {
|
||||||
|
Uri backupDirectoryUri = backupDirectorySelectionIntent.getData();
|
||||||
|
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||||
|
|
||||||
|
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
|
||||||
|
context.getContentResolver()
|
||||||
|
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
|
||||||
|
}
|
||||||
|
|
||||||
BackupPassphrase.set(context, Util.join(password, " "));
|
BackupPassphrase.set(context, Util.join(password, " "));
|
||||||
|
TextSecurePreferences.setNextBackupTime(context, 0);
|
||||||
TextSecurePreferences.setBackupEnabled(context, true);
|
TextSecurePreferences.setBackupEnabled(context, true);
|
||||||
LocalBackupListener.schedule(context);
|
LocalBackupListener.schedule(context);
|
||||||
|
|
||||||
preference.setChecked(true);
|
onBackupsEnabled.run();
|
||||||
created.dismiss();
|
created.dismiss();
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
||||||
@@ -75,16 +107,42 @@ public class BackupDialog {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
@RequiresApi(29)
|
||||||
|
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) {
|
||||||
|
new AlertDialog.Builder(fragment.requireContext())
|
||||||
|
.setView(R.layout.backup_choose_location_dialog)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
})
|
||||||
|
.setPositiveButton(R.string.BackupDialog_choose_folder, ((dialog, which) -> {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().getLatestSignalBackupDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
|
fragment.startActivityForResult(intent, requestCode);
|
||||||
|
|
||||||
|
dialog.dismiss();
|
||||||
|
}))
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.BackupDialog_delete_backups)
|
.setTitle(R.string.BackupDialog_delete_backups)
|
||||||
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
|
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
|
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
|
||||||
BackupPassphrase.set(context, null);
|
BackupUtil.disableBackups(context);
|
||||||
TextSecurePreferences.setBackupEnabled(context, false);
|
|
||||||
BackupUtil.deleteAllBackups();
|
onBackupsDisabled.run();
|
||||||
preference.setChecked(false);
|
|
||||||
})
|
})
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
|
|||||||