mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-11 04:38:54 +01:00
Compare commits
287 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c9eb880cf | ||
|
|
d7ddd85a90 | ||
|
|
7d994b2ae1 | ||
|
|
664d6475d9 | ||
|
|
a940487611 | ||
|
|
9f995d61f4 | ||
|
|
a6690e1bde | ||
|
|
d507df2e7e | ||
|
|
fa26eb2017 | ||
|
|
0b53ba8950 | ||
|
|
7447e2497b | ||
|
|
7ac83625d3 | ||
|
|
50dfe7bc25 | ||
|
|
8e32592218 | ||
|
|
a3d72fc06c | ||
|
|
f5a6d61362 | ||
|
|
bca2205945 | ||
|
|
1241f4c0e9 | ||
|
|
f6253ad0bb | ||
|
|
083301185c | ||
|
|
0273d0f285 | ||
|
|
3dc1ce3353 | ||
|
|
f8e077b824 | ||
|
|
aec2ca1d87 | ||
|
|
6e7a18ea11 | ||
|
|
fe54ec9d6c | ||
|
|
1819af3000 | ||
|
|
3c177c4883 | ||
|
|
2c871a36d0 | ||
|
|
6bde55f73b | ||
|
|
50b4e137b4 | ||
|
|
4f6d39859c | ||
|
|
45a6894da1 | ||
|
|
f71accea06 | ||
|
|
32888fa00b | ||
|
|
eba3c55ec8 | ||
|
|
21b82e291b | ||
|
|
8d9d84c4cc | ||
|
|
4c25264fbf | ||
|
|
7410d664dd | ||
|
|
c878ba3cdf | ||
|
|
97798a146f | ||
|
|
7c134a6c9d | ||
|
|
08008629b3 | ||
|
|
a57adcb2b0 | ||
|
|
7790cac0ee | ||
|
|
349ad06c45 | ||
|
|
3a75d30732 | ||
|
|
b48d4f3ec2 | ||
|
|
c92f36f9a8 | ||
|
|
faa36d417c | ||
|
|
a2b6e003b6 | ||
|
|
406af58394 | ||
|
|
bd72fc8464 | ||
|
|
05fb1a52d2 | ||
|
|
b21abb8e7e | ||
|
|
b41e602539 | ||
|
|
3f233ed39f | ||
|
|
ade6f60e76 | ||
|
|
62d85e6878 | ||
|
|
4d985255a8 | ||
|
|
fd3ef0f557 | ||
|
|
7f30300cd4 | ||
|
|
0459d118a3 | ||
|
|
c92f3b5dfd | ||
|
|
ba4d1c9844 | ||
|
|
8c3a0c1f9f | ||
|
|
1dc2a35d83 | ||
|
|
0a67731830 | ||
|
|
28d86886bd | ||
|
|
b1fcea673a | ||
|
|
eb5418787a | ||
|
|
adbda02aa4 | ||
|
|
307f47fa33 | ||
|
|
c1fb4f9421 | ||
|
|
6179c087fb | ||
|
|
ae2ba5d185 | ||
|
|
91128be8f6 | ||
|
|
8748056130 | ||
|
|
3c4e3cf048 | ||
|
|
eb48ab1784 | ||
|
|
665d9e31f6 | ||
|
|
db7272730e | ||
|
|
5787a5f68a | ||
|
|
1a21cafe6c | ||
|
|
7465818f44 | ||
|
|
62cb29fdb7 | ||
|
|
a85b08d9da | ||
|
|
b18c3ec1a9 | ||
|
|
29489a664e | ||
|
|
dbb1e50d00 | ||
|
|
2068fa8041 | ||
|
|
194975d068 | ||
|
|
b7a067e954 | ||
|
|
1e050915ef | ||
|
|
6a5c234408 | ||
|
|
7a1122b3f7 | ||
|
|
962d943a22 | ||
|
|
dbcc5d696d | ||
|
|
9232eb7c16 | ||
|
|
fc9b8f43dd | ||
|
|
5e8d74bc11 | ||
|
|
642d1984c4 | ||
|
|
0ab2100fa5 | ||
|
|
6618d696e4 | ||
|
|
c24dfdce34 | ||
|
|
214e994e90 | ||
|
|
b904de5b50 | ||
|
|
ad7c81ef4e | ||
|
|
3e8b5cdb61 | ||
|
|
6aea849a42 | ||
|
|
cd0bf470a9 | ||
|
|
c615b14c51 | ||
|
|
28bf6d300e | ||
|
|
a1095f966c | ||
|
|
58a8902d4e | ||
|
|
e582976293 | ||
|
|
143110047d | ||
|
|
c1324c7496 | ||
|
|
53eee2bd16 | ||
|
|
86b1d104d9 | ||
|
|
d1d2376210 | ||
|
|
7bede7e98a | ||
|
|
fec4a7692d | ||
|
|
b58cede072 | ||
|
|
199fb517b1 | ||
|
|
921addf4c8 | ||
|
|
61aa991d79 | ||
|
|
c1c95e1ae2 | ||
|
|
f95a29b0d4 | ||
|
|
f7bb9c85af | ||
|
|
ae30e4070c | ||
|
|
9a67c60b4e | ||
|
|
e86b26bd11 | ||
|
|
e7c259b1e9 | ||
|
|
c65761a034 | ||
|
|
0b37b0ee16 | ||
|
|
d76e58ce09 | ||
|
|
2b366f8c9c | ||
|
|
d43f7d6ad9 | ||
|
|
5b7932281e | ||
|
|
0599f76ed5 | ||
|
|
31e0f3edfb | ||
|
|
17b568e6d1 | ||
|
|
7c11962cb3 | ||
|
|
a7c4199192 | ||
|
|
8cb3909093 | ||
|
|
7480ea66ec | ||
|
|
8e94ced7b6 | ||
|
|
ffd86a96da | ||
|
|
d4cabce876 | ||
|
|
a5790edb2b | ||
|
|
d247e2eabe | ||
|
|
f4d6de466b | ||
|
|
0838c0be27 | ||
|
|
7448183ff4 | ||
|
|
8e2a265cf3 | ||
|
|
8802cebb64 | ||
|
|
0c6fe8bea3 | ||
|
|
49334ffd42 | ||
|
|
4702ab1aeb | ||
|
|
fe8fcb1394 | ||
|
|
dc1e56de4e | ||
|
|
561cca5208 | ||
|
|
a291732c1a | ||
|
|
28abc1e4ff | ||
|
|
655e43a079 | ||
|
|
94b9a458e7 | ||
|
|
bb75730315 | ||
|
|
824a8ac5f2 | ||
|
|
3baf10f0e9 | ||
|
|
cfab195e90 | ||
|
|
503d7c77a0 | ||
|
|
f99ff32947 | ||
|
|
182c758d35 | ||
|
|
f6b2d3faf8 | ||
|
|
61c7959ffc | ||
|
|
67ccd14af2 | ||
|
|
3c41b7322f | ||
|
|
d2118d0b53 | ||
|
|
89af85c893 | ||
|
|
0762a93787 | ||
|
|
570b4d7150 | ||
|
|
fc51c4940c | ||
|
|
b9ffbb8e92 | ||
|
|
de2c7d38bf | ||
|
|
c9597ef8dc | ||
|
|
a9bbee3880 | ||
|
|
2bac1a7707 | ||
|
|
80e1b2c843 | ||
|
|
7c2da69676 | ||
|
|
f25e8d602b | ||
|
|
b9c6c6b0f4 | ||
|
|
164f39e376 | ||
|
|
49190125ef | ||
|
|
555e65d3ee | ||
|
|
89b1243885 | ||
|
|
3fca46de92 | ||
|
|
aa0d7c218f | ||
|
|
2b5b664a8f | ||
|
|
784c373a0e | ||
|
|
37ae740138 | ||
|
|
fe9b8a9f47 | ||
|
|
3585667fb9 | ||
|
|
b4ed088529 | ||
|
|
bdcf390e6e | ||
|
|
c7551881b8 | ||
|
|
c131754874 | ||
|
|
c6c4988583 | ||
|
|
d43f044eb4 | ||
|
|
9c71994804 | ||
|
|
654d98b0fe | ||
|
|
c06056d847 | ||
|
|
615fbf87c9 | ||
|
|
aca3d150bf | ||
|
|
6eae2d39a8 | ||
|
|
c78e283084 | ||
|
|
2d5492ffac | ||
|
|
2830132b24 | ||
|
|
e374f3afe6 | ||
|
|
58a2e50904 | ||
|
|
be29dce7b7 | ||
|
|
e58b617689 | ||
|
|
9d3a9fc675 | ||
|
|
8bddf5206c | ||
|
|
0ac234e7bf | ||
|
|
290f84e5b1 | ||
|
|
149c138666 | ||
|
|
065a39992a | ||
|
|
4a52532256 | ||
|
|
93f541ceca | ||
|
|
e97a1b2cf6 | ||
|
|
f6b46f921c | ||
|
|
5fef0494b1 | ||
|
|
6e1621fef1 | ||
|
|
e5f1793eb3 | ||
|
|
a994712609 | ||
|
|
3b8eac0b8d | ||
|
|
52978b1b42 | ||
|
|
922d0d7203 | ||
|
|
429fdf0d76 | ||
|
|
1f718009bd | ||
|
|
c1c9ca7c4c | ||
|
|
75421b1af8 | ||
|
|
d40bb2d9ee | ||
|
|
7c8549bf5e | ||
|
|
fb8f481a87 | ||
|
|
8caa690086 | ||
|
|
d7011e3353 | ||
|
|
9af966b030 | ||
|
|
a46accfcc0 | ||
|
|
c0c4092cd9 | ||
|
|
9398716848 | ||
|
|
25234496bf | ||
|
|
1a56924a56 | ||
|
|
d168d35362 | ||
|
|
3cc2cd0b17 | ||
|
|
138b7ea796 | ||
|
|
1f1a4eb351 | ||
|
|
b9081dc942 | ||
|
|
e76808a000 | ||
|
|
e31fd8d578 | ||
|
|
7d8f780d60 | ||
|
|
0478757af4 | ||
|
|
712b0c147a | ||
|
|
fc6db45e59 | ||
|
|
5229e24397 | ||
|
|
927b6096c6 | ||
|
|
16f1128990 | ||
|
|
4fd1d05503 | ||
|
|
dfac05a118 | ||
|
|
cd869bcb89 | ||
|
|
427119cef2 | ||
|
|
dada7a4f06 | ||
|
|
44a84210d8 | ||
|
|
5ac8d3b0bd | ||
|
|
7ccba5b1c8 | ||
|
|
c2ffd8adbb | ||
|
|
7e4396ae3f | ||
|
|
d0827eb48e | ||
|
|
90397165c3 | ||
|
|
e3e47504a6 | ||
|
|
42269efa57 | ||
|
|
38adb0373d | ||
|
|
8bde389398 | ||
|
|
d29b0609a3 | ||
|
|
740977164b |
@@ -4,7 +4,7 @@ Signal is a messaging app for simple private communication with friends.
|
||||
|
||||
Signal uses your phone's data connection (WiFi/3G/4G) to communicate securely, optionally supports plain SMS/MMS to function as a unified messenger, and can also encrypt the stored messages on your phone.
|
||||
|
||||
Currently available on the Play store.
|
||||
Currently available on the Play store and [signal.org](https://signal.org/android/apk/).
|
||||
|
||||
<a href='https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height='80px'/></a>
|
||||
|
||||
@@ -59,8 +59,8 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2020 Signal
|
||||
Copyright 2013-2021 Signal
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Google Play and the Google Play logo are trademarks of Google Inc.
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
|
||||
149
app/build.gradle
149
app/build.gradle
@@ -1,7 +1,3 @@
|
||||
import org.signal.signing.ApkSignerUtil
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
@@ -27,6 +23,12 @@ repositories {
|
||||
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
|
||||
content {
|
||||
includeGroupByRegex "org\\.signal.*"
|
||||
}
|
||||
}
|
||||
maven { // textdrawable
|
||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
||||
content {
|
||||
@@ -35,11 +37,23 @@ repositories {
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
maven {
|
||||
url "https://dl.cloudsmith.io/qxAgwaeEE1vN8aLU/mobilecoin/mobilecoin/maven/"
|
||||
}
|
||||
jcenter {
|
||||
content {
|
||||
includeVersion "com.google.android.exoplayer", "exoplayer-core", "2.9.1"
|
||||
includeVersion "com.google.android.exoplayer", "exoplayer-ui", "2.9.1"
|
||||
includeVersion "com.google.android.exoplayer", "extension-mediasession", "2.9.1"
|
||||
includeVersion "mobi.upod", "time-duration-picker", "1.1.3"
|
||||
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
|
||||
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
|
||||
includeVersion "com.codewaves.stickyheadergrid", "stickyheadergrid", "0.9.4"
|
||||
includeVersion "com.amulyakhare", "com.amulyakhare.textdrawable", "1.0.1"
|
||||
includeVersion "com.google.android", "flexbox", "0.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
@@ -57,8 +71,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 884
|
||||
def canonicalVersionName = "5.18.2"
|
||||
def canonicalVersionCode = 912
|
||||
def canonicalVersionName = "5.23.0"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -69,6 +83,31 @@ def abiPostFix = ['universal' : 0,
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
def selectableVariants = [
|
||||
'internalProdFlipper',
|
||||
'internalProdPerf',
|
||||
'internalProdRelease',
|
||||
'internalStagingFlipper',
|
||||
'internalStagingPerf',
|
||||
'internalStagingRelease',
|
||||
'nightlyProdFlipper',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'nightlyStagingPerf',
|
||||
'playProdDebug',
|
||||
'playProdFlipper',
|
||||
'playProdPerf',
|
||||
'playProdRelease',
|
||||
'playStagingDebug',
|
||||
'playStagingFlipper',
|
||||
'playStagingPerf',
|
||||
'playStagingRelease',
|
||||
'studyProdMock',
|
||||
'studyProdPerf',
|
||||
'websiteProdFlipper',
|
||||
'websiteProdRelease',
|
||||
]
|
||||
|
||||
android {
|
||||
buildToolsVersion BUILD_TOOL_VERSION
|
||||
compileSdkVersion COMPILE_SDK
|
||||
@@ -110,7 +149,7 @@ android {
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||
@@ -133,7 +172,7 @@ android {
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44}"
|
||||
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44,49,33,41}"
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||
|
||||
@@ -268,7 +307,7 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\""
|
||||
}
|
||||
|
||||
study {
|
||||
@@ -295,7 +334,7 @@ android {
|
||||
|
||||
applicationIdSuffix ".staging"
|
||||
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
@@ -303,7 +342,7 @@ android {
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982\", " +
|
||||
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
@@ -339,16 +378,9 @@ android {
|
||||
def distribution = variant.getFlavors().get(0).name
|
||||
def environment = variant.getFlavors().get(1).name
|
||||
def buildType = variant.buildType.name
|
||||
def fullName = distribution + environment.capitalize() + buildType.capitalize()
|
||||
|
||||
if (distribution == 'study' && buildType != 'perf' && buildType != 'mock') {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution != 'study' && buildType == 'mock') {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution == 'internal' && buildType != 'flipper' && buildType != 'perf' && buildType != 'release') {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution == 'nightly' && environment != 'prod') {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution == 'nightly' && buildType != 'flipper' && buildType != 'perf' && buildType != 'release') {
|
||||
if (!selectableVariants.contains(fullName)) {
|
||||
variant.setIgnore(true)
|
||||
}
|
||||
}
|
||||
@@ -376,6 +408,7 @@ dependencies {
|
||||
implementation ('androidx.appcompat:appcompat:1.2.0') {
|
||||
force = true
|
||||
}
|
||||
implementation "androidx.window:window:1.0.0-alpha09"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
@@ -391,6 +424,7 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-reactivestreams-ktx:2.1.0'
|
||||
implementation "androidx.camera:camera-core:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
|
||||
@@ -409,9 +443,9 @@ dependencies {
|
||||
implementation 'com.google.android.gms:play-services-maps:16.1.0'
|
||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.15.0'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.0'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.15.0'
|
||||
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
@@ -423,7 +457,7 @@ dependencies {
|
||||
implementation project(':device-transfer')
|
||||
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
implementation 'org.whispersystems:signal-client-android:0.8.3'
|
||||
implementation "org.whispersystems:signal-client-android:${LIBSIGNAL_CLIENT_VERSION}"
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
|
||||
implementation('com.mobilecoin:android-sdk:1.1.0') {
|
||||
@@ -432,7 +466,7 @@ dependencies {
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.10.6'
|
||||
implementation 'org.signal:ringrtc-android:2.11.1'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.22"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
@@ -475,7 +509,7 @@ dependencies {
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
|
||||
implementation "net.zetetic:android-database-sqlcipher:4.4.3"
|
||||
implementation 'org.signal:android-database-sqlcipher:4.4.3-S2'
|
||||
implementation "androidx.sqlite:sqlite:2.1.0"
|
||||
|
||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||
@@ -518,67 +552,6 @@ dependencyVerification {
|
||||
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
|
||||
}
|
||||
|
||||
def assembleWebsiteDescriptor = { variant, file ->
|
||||
if (file.exists()) {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
file.eachByte 4096, {bytes, size ->
|
||||
md.update(bytes, 0, size);
|
||||
}
|
||||
|
||||
String digest = md.digest().collect {String.format "%02x", it}.join();
|
||||
String url = variant.productFlavors.get(0).ext.websiteUpdateUrl
|
||||
String apkName = file.getName()
|
||||
|
||||
String descriptor = "{" +
|
||||
"\"versionCode\" : ${canonicalVersionCode * postFixSize + abiPostFix['universal']}," +
|
||||
"\"versionName\" : \"$canonicalVersionName\"," +
|
||||
"\"sha256sum\" : \"$digest\"," +
|
||||
"\"url\" : \"$url/$apkName\"" +
|
||||
"}"
|
||||
|
||||
File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json"))
|
||||
|
||||
descriptorFile.write(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
def signProductionRelease = { variant ->
|
||||
variant.outputs.collect { output ->
|
||||
String apkName = output.outputFile.name
|
||||
File inputFile = new File(output.outputFile.path)
|
||||
File outputFile = new File(output.outputFile.parent, apkName.replace('-unsigned', ''))
|
||||
|
||||
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
|
||||
'pkcs11.config',
|
||||
'PKCS11',
|
||||
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
|
||||
outputFile.getAbsolutePath())
|
||||
|
||||
inputFile.delete()
|
||||
outputFile
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionPlayRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionInternalRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionWebsiteRelease {
|
||||
doLast {
|
||||
def variant = android.applicationVariants.find { (it.name == 'websiteProdRelease') }
|
||||
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
||||
assembleWebsiteDescriptor(variant, signedRelease)
|
||||
}
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return System.currentTimeMillis().toString()
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
-keep class org.sqlite.database.** { *; }
|
||||
|
||||
-keep class net.sqlcipher.** { *; }
|
||||
-dontwarn net.sqlcipher.**
|
||||
-dontwarn net.sqlcipher.**
|
||||
|
||||
-keep class net.zetetic.** { *; }
|
||||
-dontwarn net.zetetic.**
|
||||
|
||||
@@ -9,3 +9,5 @@
|
||||
|
||||
# Protobuf lite
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
-keep class androidx.window.** { *; }
|
||||
@@ -11,9 +11,9 @@ import androidx.annotation.Nullable;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
||||
|
||||
import net.sqlcipher.DatabaseUtils;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
import net.zetetic.database.DatabaseUtils;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
@@ -50,11 +50,13 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
||||
|
||||
return Arrays.asList(new Descriptor(mainOpenHelper),
|
||||
new Descriptor(keyValueOpenHelper),
|
||||
new Descriptor(megaphoneOpenHelper),
|
||||
new Descriptor(jobManagerOpenHelper));
|
||||
new Descriptor(jobManagerOpenHelper),
|
||||
new Descriptor(metricsOpenHelper));
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, "Unable to use reflection to access raw database.", e);
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
tools:replace="android:allowBackup"
|
||||
android:resizeableActivity="true"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:largeHeap="true">
|
||||
@@ -104,6 +105,9 @@
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
|
||||
|
||||
<meta-data android:name="android.supports_size_changes"
|
||||
android:value="true" />
|
||||
|
||||
<meta-data android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version" />
|
||||
|
||||
@@ -117,16 +121,15 @@
|
||||
<activity android:name=".WebRtcCallActivity"
|
||||
android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
|
||||
android:excludeFromRecents="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:taskAffinity=".calling"
|
||||
android:resizeableActivity="true"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:screenOrientation="portrait"
|
||||
android:noHistory="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -240,6 +243,9 @@
|
||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name=".deeplinks.DeepLinkEntryActivity"
|
||||
@@ -272,6 +278,16 @@
|
||||
<data android:scheme="sgnl"
|
||||
android:host="signal.tube" />
|
||||
</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="https"
|
||||
android:host="signal.me" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="signal.me" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".conversation.ConversationActivity"
|
||||
@@ -302,7 +318,8 @@
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
@@ -350,11 +367,11 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.MediaSendActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
<activity android:name=".mediasend.v2.MediaSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
@@ -478,6 +495,7 @@
|
||||
|
||||
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
|
||||
android:excludeFromRecents="true"
|
||||
android:permission="android.permission.CALL_PHONE"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
|
||||
@@ -174,6 +174,11 @@ public final class SignalCameraView extends FrameLayout {
|
||||
|
||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
|
||||
|
||||
// Begin custom signal code block
|
||||
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
|
||||
// End custom signal code block
|
||||
|
||||
mCameraModule = new SignalCameraXModule(this);
|
||||
|
||||
if (attrs != null) {
|
||||
|
||||
@@ -222,17 +222,10 @@ final class SignalCameraXModule {
|
||||
// End Signal Custom Code Block
|
||||
|
||||
Rational targetAspectRatio;
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
|
||||
} else {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
}
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
// End Signal Custom Code Block
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
|
||||
public final class AppCapabilities {
|
||||
@@ -8,15 +7,17 @@ public final class AppCapabilities {
|
||||
private AppCapabilities() {
|
||||
}
|
||||
|
||||
private static final boolean UUID_CAPABLE = false;
|
||||
private static final boolean GV2_CAPABLE = true;
|
||||
private static final boolean GV1_MIGRATION = true;
|
||||
private static final boolean UUID_CAPABLE = false;
|
||||
private static final boolean GV2_CAPABLE = true;
|
||||
private static final boolean GV1_MIGRATION = true;
|
||||
private static final boolean ANNOUNCEMENT_GROUPS = true;
|
||||
private static final boolean SENDER_KEY = true;
|
||||
|
||||
/**
|
||||
* @param storageCapable Whether or not the user can use storage service. This is another way of
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, FeatureFlags.senderKey());
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -113,6 +114,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
public void onCreate() {
|
||||
Tracer.getInstance().start("Application#onCreate()");
|
||||
AppStartup.getInstance().onApplicationCreate();
|
||||
SignalLocalMetrics.ColdStart.start();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
@@ -178,6 +180,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
SignalLocalMetrics.ColdStart.onApplicationCreateFinished();
|
||||
Tracer.getInstance().end("Application#onCreate()");
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
@@ -71,12 +72,14 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
|
||||
}
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
EmojiTextView title = findViewById(R.id.title);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
requireSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
|
||||
Context context = getApplicationContext();
|
||||
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
||||
@@ -122,7 +125,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
}
|
||||
});
|
||||
|
||||
toolbar.setTitle(recipient.getDisplayName(context));
|
||||
title.setText(recipient.getDisplayName(context));
|
||||
});
|
||||
|
||||
FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
@@ -24,31 +26,29 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable {
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable, Multiselectable {
|
||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<ConversationMessage> batchSelected,
|
||||
@NonNull Set<MultiselectPart> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseMention,
|
||||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
boolean canPlayInline,
|
||||
@NonNull Colorizer colorizer);
|
||||
|
||||
ConversationMessage getConversationMessage();
|
||||
@NonNull ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Base activity container for selecting a list of contacts.
|
||||
@@ -122,8 +123,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
return true;
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
callback.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -37,6 +38,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
@@ -53,6 +55,7 @@ import androidx.transition.TransitionManager;
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -88,6 +91,7 @@ import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
@@ -176,12 +180,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof OnSelectionLimitReachedListener) {
|
||||
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) getParentFragment();
|
||||
}
|
||||
|
||||
if (context instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context;
|
||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +268,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
recyclerView.setClipToPadding(recyclerViewClipping);
|
||||
|
||||
swipeRefresh.setEnabled(arguments.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, true)));
|
||||
boolean isRefreshable = arguments.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, true));
|
||||
swipeRefresh.setNestedScrollingEnabled(isRefreshable);
|
||||
swipeRefresh.setEnabled(isRefreshable);
|
||||
|
||||
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
|
||||
selectionLimit = arguments.getParcelable(SELECTION_LIMITS);
|
||||
@@ -436,6 +446,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
}
|
||||
|
||||
public void setRecyclerViewPaddingBottom(@Px int paddingBottom) {
|
||||
ViewUtil.setPaddingBottom(recyclerView, paddingBottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
FragmentActivity activity = requireActivity();
|
||||
@@ -566,28 +580,32 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
|
||||
if (allowed) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (onContactSelectedListener != null) {
|
||||
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber(), allowed -> {
|
||||
if (allowed) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
@@ -680,6 +698,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
@Override
|
||||
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||
if (getView() == null || !requireView().isAttachedToWindow()) {
|
||||
Log.w(TAG, "Fragment's view was detached before the animation completed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (view == chip && transitionType == LayoutTransition.APPEARING) {
|
||||
chipGroup.getLayoutTransition().removeTransitionListener(this);
|
||||
registerChipRecipientObserver(chip, recipient.live());
|
||||
@@ -742,8 +765,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
/** @return True if the contact is allowed to be selected, otherwise false. */
|
||||
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
/** Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it. */
|
||||
void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
void onSelectionChanged();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ package org.thoughtcrime.securesms;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
@@ -14,12 +12,12 @@ import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AnimRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
@@ -34,27 +32,25 @@ import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
private ViewGroup smsSendFrame;
|
||||
private Button smsSendButton;
|
||||
private Animation slideInAnimation;
|
||||
private Animation slideOutAnimation;
|
||||
private DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme();
|
||||
private Toolbar primaryToolbar;
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
private ViewGroup smsSendFrame;
|
||||
private Button smsSendButton;
|
||||
private Animation slideInAnimation;
|
||||
private Animation slideOutAnimation;
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme();
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
@@ -82,7 +78,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
private void initializeAppBar() {
|
||||
primaryToolbar = findViewById(R.id.toolbar);
|
||||
final Toolbar primaryToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(primaryToolbar);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
@@ -96,9 +92,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
slideOutAnimation = loadAnimation(R.anim.slide_to_bottom);
|
||||
|
||||
View shareButton = findViewById(R.id.share_button);
|
||||
Button smsButton = findViewById(R.id.sms_button);
|
||||
TextView shareText = findViewById(R.id.share_text);
|
||||
View smsButton = findViewById(R.id.sms_button);
|
||||
Button smsCancelButton = findViewById(R.id.cancel_sms_button);
|
||||
Toolbar smsToolbar = findViewById(R.id.sms_send_frame_toolbar);
|
||||
ContactFilterView contactFilter = findViewById(R.id.contact_filter_edit_text);
|
||||
|
||||
inviteText = findViewById(R.id.invite_text);
|
||||
@@ -120,15 +116,14 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
smsToolbar.setNavigationIcon(R.drawable.ic_search_conversation_24);
|
||||
|
||||
if (Util.isDefaultSmsProvider(this)) {
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
smsButton.setOnClickListener(new SmsClickListener());
|
||||
} else {
|
||||
shareButton.setVisibility(View.GONE);
|
||||
smsButton.setOnClickListener(new ShareClickListener());
|
||||
smsButton.setText(R.string.InviteActivity_share);
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,9 +134,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||
return true;
|
||||
callback.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,9 +156,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
private void updateSmsButtonText(int count) {
|
||||
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
|
||||
count,
|
||||
count));
|
||||
smsSendButton.setText(getResources().getString(R.string.InviteActivity_send_sms, count));
|
||||
smsSendButton.setEnabled(count > 0);
|
||||
}
|
||||
|
||||
@@ -175,43 +168,21 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
}
|
||||
|
||||
@Override public boolean onSupportNavigateUp() {
|
||||
if (smsSendFrame.getVisibility() == View.VISIBLE) {
|
||||
cancelSmsSelection();
|
||||
return false;
|
||||
} else {
|
||||
return super.onSupportNavigateUp();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelSmsSelection() {
|
||||
setPrimaryColorsToolbarNormal();
|
||||
contactsFragment.reset();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
||||
}
|
||||
|
||||
private void setPrimaryColorsToolbarNormal() {
|
||||
primaryToolbar.setBackgroundColor(0);
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(null);
|
||||
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_primary));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
WindowUtil.setStatusBarColor(getWindow(), ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
|
||||
getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor));
|
||||
WindowUtil.setLightStatusBarFromTheme(this);
|
||||
}
|
||||
|
||||
WindowUtil.setLightNavigationBarFromTheme(this);
|
||||
}
|
||||
|
||||
private void setPrimaryColorsToolbarForSms() {
|
||||
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(ContextCompat.getColor(this, R.color.signal_text_toolbar_subtitle), PorterDuff.Mode.SRC_IN);
|
||||
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_toolbar_title));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
WindowUtil.setStatusBarColor(getWindow(), ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
WindowUtil.clearLightStatusBar(getWindow());
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
WindowUtil.clearLightNavigationBar(getWindow());
|
||||
}
|
||||
}
|
||||
|
||||
private class ShareClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -230,7 +201,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
private class SmsClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
setPrimaryColorsToolbarForSms();
|
||||
ViewUtil.animateIn(smsSendFrame, slideInAnimation);
|
||||
}
|
||||
}
|
||||
@@ -282,7 +252,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
|
||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null);
|
||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
|
||||
|
||||
if (recipient.getContactUri() != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setHasSentInvite(recipient.getId());
|
||||
|
||||
@@ -49,6 +49,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
handleProxyInIntent(getIntent());
|
||||
handleSignalMeIntent(getIntent());
|
||||
|
||||
CachedInflater.from(this).clear();
|
||||
}
|
||||
@@ -65,6 +66,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
super.onNewIntent(intent);
|
||||
handleGroupLinkInIntent(intent);
|
||||
handleProxyInIntent(intent);
|
||||
handleSignalMeIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -115,6 +117,13 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSignalMeIntent(Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
CommunicationActions.handlePotentialSignalMeUrl(this, data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
|
||||
return mediaController;
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Activity container for starting a new conversation.
|
||||
@@ -60,7 +61,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
@@ -94,7 +95,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
callback.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -35,7 +35,6 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.BounceInterpolator;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
@@ -51,6 +50,7 @@ import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricManager.Authenticators;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
@@ -98,13 +98,16 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
private boolean hadFailure;
|
||||
private boolean alreadyShown;
|
||||
|
||||
private final Runnable resumeScreenLockRunnable = () -> {
|
||||
resumeScreenLock(!alreadyShown);
|
||||
alreadyShown = true;
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Log.i(TAG, "onCreate()");
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.prompt_passphrase_activity);
|
||||
@@ -129,11 +132,20 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
setLockTypeVisibility();
|
||||
|
||||
if (TextSecurePreferences.isScreenLockEnabled(this) && !authenticated && !hadFailure) {
|
||||
resumeScreenLock(!alreadyShown);
|
||||
alreadyShown = true;
|
||||
ThreadUtil.postToMain(resumeScreenLockRunnable);
|
||||
}
|
||||
|
||||
hadFailure = false;
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
ThreadUtil.cancelRunnableOnMain(resumeScreenLockRunnable);
|
||||
biometricPrompt.cancelAuthentication();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -388,9 +400,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
handleAuthenticated();
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@@ -412,7 +421,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,30 +45,32 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnticipateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextSwitcher;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.core.view.OneShotPreDrawListener;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ShapeScrim;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -115,7 +117,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityDatabase.IdentityRecord identityRecord)
|
||||
@NonNull IdentityRecord identityRecord)
|
||||
{
|
||||
return newIntent(context,
|
||||
identityRecord.getRecipientId(),
|
||||
@@ -124,7 +126,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityDatabase.IdentityRecord identityRecord,
|
||||
@NonNull IdentityRecord identityRecord,
|
||||
boolean verified)
|
||||
{
|
||||
return newIntent(context,
|
||||
@@ -214,7 +216,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||
public static class VerifyDisplayFragment extends Fragment {
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
public static final String REMOTE_NUMBER = "remote_number";
|
||||
@@ -230,23 +232,28 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
private View container;
|
||||
private View numbersContainer;
|
||||
private View loading;
|
||||
private View qrCodeContainer;
|
||||
private ImageView qrCode;
|
||||
private ImageView qrVerified;
|
||||
private TextView tapLabel;
|
||||
private TextSwitcher tapLabel;
|
||||
private TextView description;
|
||||
private View.OnClickListener clickListener;
|
||||
private SwitchCompat verified;
|
||||
private Button verifyButton;
|
||||
|
||||
private TextView[] codes = new TextView[12];
|
||||
private boolean animateSuccessOnDraw = false;
|
||||
private boolean animateFailureOnDraw = false;
|
||||
private boolean currentVerifiedState = false;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
|
||||
this.numbersContainer = container.findViewById(R.id.number_table);
|
||||
this.loading = container.findViewById(R.id.loading);
|
||||
this.qrCodeContainer = container.findViewById(R.id.qr_code_container);
|
||||
this.qrCode = container.findViewById(R.id.qr_code);
|
||||
this.verified = container.findViewById(R.id.verified_switch);
|
||||
this.verifyButton = container.findViewById(R.id.verify_button);
|
||||
this.qrVerified = container.findViewById(R.id.qr_verified);
|
||||
this.description = container.findViewById(R.id.description);
|
||||
this.tapLabel = container.findViewById(R.id.tap_label);
|
||||
@@ -263,11 +270,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
this.codes[10] = container.findViewById(R.id.code_eleventh);
|
||||
this.codes[11] = container.findViewById(R.id.code_twelth);
|
||||
|
||||
this.qrCode.setOnClickListener(clickListener);
|
||||
this.qrCodeContainer.setOnClickListener(clickListener);
|
||||
this.registerForContextMenu(numbersContainer);
|
||||
|
||||
this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
|
||||
this.verified.setOnCheckedChangeListener(this);
|
||||
updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false);
|
||||
this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true)));
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -327,6 +334,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Fingerprint fingerprint) {
|
||||
if (getActivity() == null) return;
|
||||
VerifyDisplayFragment.this.fingerprint = fingerprint;
|
||||
setFingerprintViews(fingerprint, true);
|
||||
getActivity().supportInvalidateOptionsMenu();
|
||||
@@ -480,7 +488,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
private void setRecipientText(Recipient recipient) {
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
@@ -501,9 +509,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
if (animate) {
|
||||
ViewUtil.fadeIn(qrCode, 1000);
|
||||
ViewUtil.fadeIn(tapLabel, 1000);
|
||||
ViewUtil.fadeOut(loading, 300, View.GONE);
|
||||
} else {
|
||||
qrCode.setVisibility(View.VISIBLE);
|
||||
tapLabel.setVisibility(View.VISIBLE);
|
||||
loading.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +569,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__successful_match));
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
@@ -569,6 +581,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__failed_to_verify_safety_number));
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
@@ -576,7 +590,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
scaleAnimation.setInterpolator(new OvershootInterpolator());
|
||||
scaleAnimation.setInterpolator(new FastOutSlowInInterpolator());
|
||||
scaleAnimation.setDuration(800);
|
||||
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
@@ -594,6 +608,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
scaleAnimation.setInterpolator(new AnticipateInterpolator());
|
||||
scaleAnimation.setDuration(500);
|
||||
ViewUtil.animateOut(qrVerified, scaleAnimation, View.GONE);
|
||||
ViewUtil.fadeIn(qrCode, 800);
|
||||
qrCodeContainer.setEnabled(true);
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__tap_to_scan));
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
@@ -602,53 +619,74 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
|
||||
ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE);
|
||||
ViewUtil.animateIn(qrVerified, scaleAnimation);
|
||||
qrCodeContainer.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
|
||||
final Recipient recipient = this.recipient.get();
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
private void updateVerifyButton(boolean verified, boolean update) {
|
||||
currentVerifiedState = verified;
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (isChecked) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.saveIdentity(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED, false,
|
||||
System.currentTimeMillis(), true);
|
||||
} else {
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.setVerified(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.DEFAULT);
|
||||
if (verified) {
|
||||
verifyButton.setText(R.string.verify_display_fragment__clear_verification);
|
||||
} else {
|
||||
verifyButton.setText(R.string.verify_display_fragment__mark_as_verified);
|
||||
}
|
||||
|
||||
if (update) {
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (verified) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
ApplicationDependencies.getIdentityStore()
|
||||
.saveIdentityWithoutSideEffects(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
true);
|
||||
} else {
|
||||
ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, VerifiedStatus.DEFAULT);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
|
||||
remoteIdentity,
|
||||
verified ? VerifiedStatus.VERIFIED
|
||||
: VerifiedStatus.DEFAULT));
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), verified, false);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
|
||||
remoteIdentity,
|
||||
isChecked ? VerifiedStatus.VERIFIED
|
||||
: VerifiedStatus.DEFAULT));
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient, isChecked, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class VerifyScanFragment extends Fragment {
|
||||
|
||||
private View container;
|
||||
private CameraView cameraView;
|
||||
private ShapeScrim cameraScrim;
|
||||
private ImageView cameraMarks;
|
||||
private ScanningThread scanningThread;
|
||||
private ScanListener scanListener;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = container.findViewById(R.id.scanner);
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = container.findViewById(R.id.scanner);
|
||||
this.cameraScrim = container.findViewById(R.id.camera_scrim);
|
||||
this.cameraMarks = container.findViewById(R.id.camera_marks);
|
||||
|
||||
OneShotPreDrawListener.add(cameraScrim, () -> {
|
||||
int width = cameraScrim.getScrimWidth();
|
||||
int height = cameraScrim.getScrimHeight();
|
||||
|
||||
ViewUtil.updateLayoutParams(cameraMarks, width, height);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -17,29 +17,39 @@
|
||||
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Rational;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.window.DisplayFeature;
|
||||
import androidx.window.FoldingFeature;
|
||||
import androidx.window.WindowLayoutInfo;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
||||
@@ -50,6 +60,7 @@ import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeN
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
|
||||
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -61,17 +72,19 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
@@ -88,11 +101,14 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
|
||||
private DeviceOrientationMonitor deviceOrientationMonitor;
|
||||
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
private WebRtcCallView callScreen;
|
||||
private TooltipPopup videoTooltip;
|
||||
private WebRtcCallViewModel viewModel;
|
||||
private boolean enableVideoIfAvailable;
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
private WebRtcCallView callScreen;
|
||||
private TooltipPopup videoTooltip;
|
||||
private WebRtcCallViewModel viewModel;
|
||||
private boolean enableVideoIfAvailable;
|
||||
private androidx.window.WindowManager windowManager;
|
||||
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
|
||||
private ThrottledDebouncer requestNewSizesThrottle;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
@@ -100,6 +116,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Log.i(TAG, "onCreate()");
|
||||
@@ -107,6 +124,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
boolean isLandscapeEnabled = getResources().getConfiguration().smallestScreenWidthDp >= 480;
|
||||
if (!isLandscapeEnabled) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.webrtc_call_activity);
|
||||
|
||||
@@ -115,12 +137,19 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||
|
||||
initializeResources();
|
||||
initializeViewModel();
|
||||
initializeViewModel(isLandscapeEnabled);
|
||||
|
||||
processIntent(getIntent());
|
||||
|
||||
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
|
||||
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
|
||||
|
||||
windowManager = new androidx.window.WindowManager(this);
|
||||
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
|
||||
|
||||
windowManager.registerLayoutChangeCallback(SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
|
||||
|
||||
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -165,6 +194,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
if (!isInPipMode() || isFinishing()) {
|
||||
EventBus.getDefault().unregister(this);
|
||||
requestNewSizesThrottle.clear();
|
||||
}
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
@@ -178,6 +208,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
windowManager.unregisterLayoutChangeCallback(windowLayoutInfoConsumer);
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@@ -208,8 +239,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private boolean enterPipModeIfPossible() {
|
||||
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
|
||||
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(9, 16))
|
||||
.build();
|
||||
.setAspectRatio(new Rational(9, 16))
|
||||
.build();
|
||||
enterPictureInPictureMode(params);
|
||||
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
|
||||
|
||||
@@ -248,53 +279,46 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen);
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
private void initializeViewModel(boolean isLandscapeEnabled) {
|
||||
deviceOrientationMonitor = new DeviceOrientationMonitor(this);
|
||||
getLifecycle().addObserver(deviceOrientationMonitor);
|
||||
|
||||
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class);
|
||||
viewModel.setIsLandscapeEnabled(isLandscapeEnabled);
|
||||
viewModel.setIsInPipMode(isInPipMode());
|
||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(), viewModel.getOrientation(), (s, o) -> new Pair<>(s, o == PORTRAIT_BOTTOM_EDGE))
|
||||
.observe(this, p -> callScreen.updateCallParticipants(p.first(), p.second()));
|
||||
|
||||
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
|
||||
viewModel.getOrientationAndLandscapeEnabled(),
|
||||
(s, o) -> new CallParticipantsViewState(s, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
|
||||
.observe(this, p -> callScreen.updateCallParticipants(p));
|
||||
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
|
||||
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
|
||||
viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall());
|
||||
viewModel.getGroupMembersChanged().observe(this, unused -> updateGroupMembersForGroupCall());
|
||||
viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange);
|
||||
viewModel.shouldShowSpeakerHint().observe(this, this::updateSpeakerHint);
|
||||
|
||||
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null) {
|
||||
if (state.needsNewRequestSizes()) {
|
||||
ApplicationDependencies.getSignalCallManager().updateRenderedResolutions();
|
||||
requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getOrientation().observe(this, orientation -> {
|
||||
ApplicationDependencies.getSignalCallManager().orientationChanged(orientation.getDegrees());
|
||||
|
||||
switch (orientation) {
|
||||
case LANDSCAPE_LEFT_EDGE:
|
||||
callScreen.rotateControls(90);
|
||||
break;
|
||||
case LANDSCAPE_RIGHT_EDGE:
|
||||
callScreen.rotateControls(-90);
|
||||
break;
|
||||
case PORTRAIT_BOTTOM_EDGE:
|
||||
callScreen.rotateControls(0);
|
||||
}
|
||||
});
|
||||
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
|
||||
viewModel.getControlsRotation().observe(this, callScreen::rotateControls);
|
||||
}
|
||||
|
||||
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
||||
if (event instanceof WebRtcCallViewModel.Event.StartCall) {
|
||||
startCall(((WebRtcCallViewModel.Event.StartCall)event).isVideoCall());
|
||||
startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall());
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
|
||||
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
|
||||
@@ -406,7 +430,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
|
||||
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
||||
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setRecipient(recipient);
|
||||
@@ -490,13 +514,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
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
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.RedPhone_number_not_registered)
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.show();
|
||||
.setTitle(R.string.RedPhone_number_not_registered)
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
||||
@@ -526,6 +550,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers();
|
||||
}
|
||||
|
||||
public void handleGroupMemberCountChange(int count) {
|
||||
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize() && FeatureFlags.groupCallRinging();
|
||||
callScreen.enableRingGroup(canRing);
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
|
||||
}
|
||||
|
||||
private void updateSpeakerHint(boolean showSpeakerHint) {
|
||||
if (showSpeakerHint) {
|
||||
callScreen.showSpeakerViewHint();
|
||||
@@ -588,20 +618,34 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
|
||||
switch (event.getState()) {
|
||||
case CALL_PRE_JOIN: handleCallPreJoin(event); break;
|
||||
case CALL_CONNECTED: handleCallConnected(event); break;
|
||||
case NETWORK_FAILURE: handleServerFailure(); break;
|
||||
case CALL_RINGING: handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); 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_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
||||
case CALL_BUSY: handleCallBusy(); break;
|
||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||
case CALL_PRE_JOIN:
|
||||
handleCallPreJoin(event); break;
|
||||
case CALL_CONNECTED:
|
||||
handleCallConnected(event); break;
|
||||
case NETWORK_FAILURE:
|
||||
handleServerFailure(); break;
|
||||
case CALL_RINGING:
|
||||
handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); 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_ONGOING_ELSEWHERE:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||
case CALL_NEEDS_PERMISSION:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||
case NO_SUCH_USER:
|
||||
handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE:
|
||||
handleRecipientUnavailable(); break;
|
||||
case CALL_OUTGOING:
|
||||
handleOutgoingCall(event); break;
|
||||
case CALL_BUSY:
|
||||
handleCallBusy(); break;
|
||||
case UNTRUSTED_IDENTITY:
|
||||
handleUntrustedIdentity(event); break;
|
||||
}
|
||||
|
||||
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
@@ -617,6 +661,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
|
||||
if (event.getGroupState().isNotIdle()) {
|
||||
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||
callScreen.setRingGroup(event.shouldRingGroup());
|
||||
|
||||
if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,5 +780,38 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
public void onLocalPictureInPictureClicked() {
|
||||
viewModel.onLocalPictureInPictureClicked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) {
|
||||
if (ringingAllowed) {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(ringGroup);
|
||||
} else {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
|
||||
Toast.makeText(WebRtcCallActivity.this, R.string.WebRtcCallActivity__group_is_too_large_to_ring_the_participants, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {
|
||||
|
||||
@Override
|
||||
public void accept(WindowLayoutInfo windowLayoutInfo) {
|
||||
Log.d(TAG, "On WindowLayoutInfo accepted: " + windowLayoutInfo.toString());
|
||||
|
||||
Optional<DisplayFeature> feature = windowLayoutInfo.getDisplayFeatures().stream().filter(f -> f instanceof FoldingFeature).findFirst();
|
||||
viewModel.setIsLandscapeEnabled(feature.isPresent());
|
||||
setRequestedOrientation(feature.isPresent() ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
if (feature.isPresent()) {
|
||||
FoldingFeature foldingFeature = (FoldingFeature) feature.get();
|
||||
Rect bounds = foldingFeature.getBounds();
|
||||
if (foldingFeature.getState() == FoldingFeature.State.HALF_OPENED && bounds.top == bounds.bottom) {
|
||||
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode");
|
||||
viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top));
|
||||
} else {
|
||||
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in flat display mode");
|
||||
viewModel.setFoldableState(WebRtcControls.FoldableState.flat());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public class AudioCodec {
|
||||
private final AudioRecord audioRecord;
|
||||
|
||||
private boolean running = true;
|
||||
private boolean failed = false;
|
||||
private boolean finished = false;
|
||||
|
||||
public AudioCodec() throws IOException {
|
||||
@@ -76,10 +77,25 @@ public class AudioCodec {
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
} finally {
|
||||
mediaCodec.stop();
|
||||
audioRecord.stop();
|
||||
|
||||
mediaCodec.release();
|
||||
try {
|
||||
mediaCodec.stop();
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.w(TAG, "mediaCodec stop failed.", ise);
|
||||
}
|
||||
|
||||
try {
|
||||
audioRecord.stop();
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.w(TAG, "audioRecord stop failed.", ise);
|
||||
}
|
||||
|
||||
try {
|
||||
mediaCodec.release();
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.w(TAG, "mediaCodec release failed. Probably already released.", ise);
|
||||
}
|
||||
|
||||
audioRecord.release();
|
||||
|
||||
StreamUtil.close(outputStream);
|
||||
|
||||
@@ -30,6 +30,7 @@ class TextAvatarDrawable(
|
||||
textView.text = avatar.text
|
||||
textView.gravity = Gravity.CENTER
|
||||
textView.setTextColor(if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor)
|
||||
textView.setForceCustomEmoji(true)
|
||||
|
||||
layout.addView(textView)
|
||||
|
||||
|
||||
@@ -57,6 +57,16 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelEditing() {
|
||||
Navigation.findNavController(requireView()).popBackStack()
|
||||
}
|
||||
|
||||
override fun onMainImageLoaded() {
|
||||
}
|
||||
|
||||
override fun onMainImageFailedToLoad() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY_EDIT = "org.thoughtcrime.securesms.avatar.photo.EDIT"
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
|
||||
@@ -47,6 +46,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
|
||||
private val viewModel: AvatarPickerViewModel by viewModels(factoryProducer = this::createFactory)
|
||||
|
||||
private lateinit var recycler: RecyclerView
|
||||
|
||||
private fun createFactory(): AvatarPickerViewModel.Factory {
|
||||
val args = AvatarPickerFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = ParcelableGroupId.get(args.groupId)
|
||||
@@ -56,13 +57,13 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.avatar_picker_toolbar)
|
||||
val recycler: RecyclerView = view.findViewById(R.id.avatar_picker_recycler)
|
||||
val cameraButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_camera)
|
||||
val photoButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_photo)
|
||||
val textButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_text)
|
||||
val saveButton: View = view.findViewById(R.id.avatar_picker_save)
|
||||
val clearButton: View = view.findViewById(R.id.avatar_picker_clear)
|
||||
|
||||
recycler = view.findViewById(R.id.avatar_picker_recycler)
|
||||
recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16)))
|
||||
|
||||
val adapter = MappingAdapter()
|
||||
@@ -87,7 +88,13 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
saveButton.animate().alpha(alpha)
|
||||
}
|
||||
|
||||
adapter.submitList(state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) })
|
||||
val items = state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) }
|
||||
val selectedPosition = items.indexOfFirst { it.isSelected }
|
||||
|
||||
adapter.submitList(items) {
|
||||
if (selectedPosition > -1)
|
||||
recycler.smoothScrollToPosition(selectedPosition)
|
||||
}
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener { Navigation.findNavController(it).popBackStack() }
|
||||
@@ -141,7 +148,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
val media: Media = Objects.requireNonNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
|
||||
val media: Media = requireNotNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
|
||||
viewModel.onAvatarPhotoSelectionCompleted(media)
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
@@ -187,23 +194,23 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
}
|
||||
}
|
||||
|
||||
fun openPhotoEditor(photo: Avatar.Photo) {
|
||||
private fun openPhotoEditor(photo: Avatar.Photo) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
|
||||
}
|
||||
|
||||
fun openVectorEditor(vector: Avatar.Vector) {
|
||||
private fun openVectorEditor(vector: Avatar.Vector) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
|
||||
}
|
||||
|
||||
fun openTextEditor(text: Avatar.Text?) {
|
||||
private fun openTextEditor(text: Avatar.Text?) {
|
||||
val bundle = if (text != null) AvatarBundler.bundleText(text) else null
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
|
||||
}
|
||||
|
||||
fun openCameraCapture() {
|
||||
private fun openCameraCapture() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
@@ -218,7 +225,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
.execute()
|
||||
}
|
||||
|
||||
fun openGallery() {
|
||||
private fun openGallery() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import org.signal.core.util.EditTextUtil
|
||||
@@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerItem
|
||||
import org.thoughtcrime.securesms.components.BoldSelectionTabItem
|
||||
import org.thoughtcrime.securesms.components.ControllableTabLayout
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
@@ -60,6 +62,7 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
||||
val toolbar: Toolbar = view.findViewById(R.id.text_avatar_creation_toolbar)
|
||||
val tabLayout: ControllableTabLayout = view.findViewById(R.id.text_avatar_creation_tabs)
|
||||
val doneButton: View = view.findViewById(R.id.text_avatar_creation_done)
|
||||
val keyboardAwareLayout: KeyboardAwareLinearLayout = view.findViewById(R.id.keyboard_aware_layout)
|
||||
|
||||
withRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content)
|
||||
withoutRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content_hidden_recycler)
|
||||
@@ -111,6 +114,15 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
keyboardAwareLayout.addOnKeyboardHiddenListener {
|
||||
if (tabLayout.selectedTabPosition == 1) {
|
||||
val transition = AutoTransition().setStartDelay(250L)
|
||||
TransitionManager.endTransitions(content)
|
||||
withRecyclerSet.applyTo(content)
|
||||
TransitionManager.beginDelayedTransition(content, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OnTabSelectedListener : TabLayout.OnTabSelectedListener {
|
||||
@@ -120,18 +132,12 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
||||
textInput.isEnabled = true
|
||||
ViewUtil.focusAndShowKeyboard(textInput)
|
||||
|
||||
TransitionManager.endTransitions(content)
|
||||
withoutRecyclerSet.applyTo(content)
|
||||
TransitionManager.beginDelayedTransition(content)
|
||||
textInput.setSelection(textInput.length())
|
||||
}
|
||||
1 -> {
|
||||
textInput.isEnabled = false
|
||||
ViewUtil.hideKeyboard(requireContext(), textInput)
|
||||
|
||||
TransitionManager.endTransitions(content)
|
||||
withRecyclerSet.applyTo(content)
|
||||
TransitionManager.beginDelayedTransition(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.documentfile.provider.DocumentFile;
|
||||
import com.annimon.stream.function.Predicate;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -84,7 +85,8 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
EmojiSearchDatabase.TABLE_NAME,
|
||||
SenderKeyDatabase.TABLE_NAME,
|
||||
SenderKeySharedDatabase.TABLE_NAME,
|
||||
PendingRetryReceiptDatabase.TABLE_NAME
|
||||
PendingRetryReceiptDatabase.TABLE_NAME,
|
||||
AvatarPickerDatabase.TABLE_NAME
|
||||
);
|
||||
|
||||
public static void export(@NonNull Context context,
|
||||
|
||||
@@ -11,7 +11,7 @@ import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.blocked;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
@@ -8,10 +7,12 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
@@ -25,6 +26,8 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class BlockedUsersActivity extends PassphraseRequiredActivity implements BlockedUsersFragment.Listener, ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private static final String CONTACT_SELECTION_FRAGMENT = "Contact.Selection.Fragment";
|
||||
@@ -84,24 +87,24 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
final String displayName = recipientId.transform(id -> Recipient.resolved(id).getDisplayName(this)).or(number);
|
||||
|
||||
AlertDialog confirmationDialog = new AlertDialog.Builder(BlockedUsersActivity.this)
|
||||
.setTitle(R.string.BlockedUsersActivity__block_user)
|
||||
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
|
||||
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
|
||||
if (recipientId.isPresent()) {
|
||||
viewModel.block(recipientId.get());
|
||||
} else {
|
||||
viewModel.createAndBlock(number);
|
||||
}
|
||||
dialog.dismiss();
|
||||
onBackPressed();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
AlertDialog confirmationDialog = new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.BlockedUsersActivity__block_user)
|
||||
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
|
||||
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
|
||||
if (recipientId.isPresent()) {
|
||||
viewModel.block(recipientId.get());
|
||||
} else {
|
||||
viewModel.createAndBlock(number);
|
||||
}
|
||||
dialog.dismiss();
|
||||
onBackPressed();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
|
||||
confirmationDialog.setOnShowListener(dialog -> {
|
||||
confirmationDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.RED);
|
||||
@@ -109,7 +112,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
|
||||
confirmationDialog.show();
|
||||
|
||||
return false;
|
||||
callback.accept(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -63,7 +64,7 @@ final class BlockedUsersAdapter extends ListAdapter<Recipient, BlockedUsersAdapt
|
||||
displayName.setText(recipient.getDisplayName(itemView.getContext()));
|
||||
|
||||
if (recipient.hasAUserSetDisplayName(itemView.getContext())) {
|
||||
String identifier = recipient.getE164().or(recipient.getUsername()).orNull();
|
||||
String identifier = recipient.getE164().transform(PhoneNumberFormatter::prettyPrint).or(recipient.getUsername()).orNull();
|
||||
|
||||
if (identifier != null) {
|
||||
numberOrUsername.setText(identifier);
|
||||
|
||||
@@ -346,7 +346,7 @@ public final class AudioView extends FrameLayout {
|
||||
}
|
||||
|
||||
if (duration != null && durationMillis > 0) {
|
||||
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
|
||||
long remainingSecs = Math.max(0, TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis));
|
||||
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
@@ -101,7 +102,6 @@ public final class ContactFilterView extends FrameLayout {
|
||||
|
||||
expandTapArea(toggleContainer, dialpadToggle);
|
||||
applyAttributes(searchText, context, attrs, defStyleAttr);
|
||||
searchText.requestFocus();
|
||||
}
|
||||
|
||||
private void applyAttributes(@NonNull EditText searchText,
|
||||
@@ -121,6 +121,16 @@ public final class ContactFilterView extends FrameLayout {
|
||||
if (!attributes.getBoolean(R.styleable.ContactFilterToolbar_showDialpad, true)) {
|
||||
dialpadToggle.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (attributes.getBoolean(R.styleable.ContactFilterToolbar_cfv_autoFocus, true)) {
|
||||
searchText.requestFocus();
|
||||
}
|
||||
|
||||
int backgroundRes = attributes.getResourceId(R.styleable.ContactFilterToolbar_cfv_background, -1);
|
||||
if (backgroundRes != -1) {
|
||||
findViewById(R.id.background_holder).setBackgroundResource(backgroundRes);
|
||||
}
|
||||
|
||||
attributes.recycle();
|
||||
}
|
||||
|
||||
@@ -137,6 +147,10 @@ public final class ContactFilterView extends FrameLayout {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setOnSearchInputFocusChangedListener(@Nullable OnFocusChangeListener listener) {
|
||||
searchText.setOnFocusChangeListener(listener);
|
||||
}
|
||||
|
||||
public void setHint(@StringRes int hint) {
|
||||
searchText.setHint(hint);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import com.airbnb.lottie.LottieProperty;
|
||||
import com.airbnb.lottie.model.KeyPath;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -33,15 +32,18 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ConversationItemFooter extends ConstraintLayout {
|
||||
@@ -63,6 +65,8 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
private final Rect speedToggleHitRect = new Rect();
|
||||
private final int touchTargetSize = ViewUtil.dpToPx(48);
|
||||
|
||||
private long previousMessageId;
|
||||
|
||||
public ConversationItemFooter(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
@@ -146,7 +150,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
|
||||
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
|
||||
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis);
|
||||
long remainingSecs = Math.max(0, TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis));
|
||||
audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||
}
|
||||
|
||||
@@ -211,6 +215,10 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public TextView getDateView() {
|
||||
return dateView;
|
||||
}
|
||||
|
||||
private void notifyTouchDelegateChanged(@NonNull Rect rect, @NonNull View touchDelegate) {
|
||||
if (onTouchDelegateChangedListener != null) {
|
||||
onTouchDelegateChangedListener.onTouchDelegateChanged(rect, touchDelegate);
|
||||
@@ -307,7 +315,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
} else if (messageRecord.isRateLimited()) {
|
||||
dateView.setText(R.string.ConversationItem_send_paused);
|
||||
} else {
|
||||
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
||||
dateView.setText(DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +375,19 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
|
||||
private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
|
||||
long newMessageId = buildMessageId(messageRecord);
|
||||
|
||||
if (previousMessageId == newMessageId && deliveryStatusView.isPending() && !messageRecord.isPending()) {
|
||||
if (messageRecord.getRecipient().isGroup()) {
|
||||
SignalLocalMetrics.GroupMessageSend.onUiUpdated(messageRecord.getId());
|
||||
} else {
|
||||
SignalLocalMetrics.IndividualMessageSend.onUiUpdated(messageRecord.getId());
|
||||
}
|
||||
}
|
||||
|
||||
previousMessageId = newMessageId;
|
||||
|
||||
|
||||
if (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback()) {
|
||||
deliveryStatusView.setNone();
|
||||
return;
|
||||
@@ -400,7 +421,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) {
|
||||
showAudioDurationViews();
|
||||
|
||||
if (messageRecord.getViewedReceiptCount() > 0) {
|
||||
if (messageRecord.getViewedReceiptCount() > 0 || (messageRecord.isOutgoing() && Objects.equals(messageRecord.getRecipient(), Recipient.self()))) {
|
||||
revealDot.setProgress(1f);
|
||||
} else {
|
||||
revealDot.setProgress(0f);
|
||||
@@ -425,6 +446,10 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
playbackSpeedToggleTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private long buildMessageId(@NonNull MessageRecord record) {
|
||||
return record.isMms() ? -record.getId() : record.getId();
|
||||
}
|
||||
|
||||
public interface OnTouchDelegateChangedListener {
|
||||
void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,10 @@ public class DeliveryStatusView extends FrameLayout {
|
||||
this.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return pendingIndicator.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
public void setPending() {
|
||||
this.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
* Forces rounded corners on BottomSheet
|
||||
*/
|
||||
abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
protected open val peekHeightPercentage: Float = 0.5f
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, R.style.Widget_Signal_FixedRoundedCorners)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
|
||||
|
||||
dialog.behavior.peekHeight = (resources.displayMetrics.heightPixels * peekHeightPercentage).toInt()
|
||||
|
||||
val shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setTopLeftCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18).toFloat())
|
||||
.setTopRightCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18).toFloat())
|
||||
.build()
|
||||
|
||||
val dialogBackground = MaterialShapeDrawable(shapeAppearanceModel)
|
||||
|
||||
dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.signal_background_dialog))
|
||||
|
||||
dialog.behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (bottomSheet.background !== dialogBackground) {
|
||||
ViewCompat.setBackground(bottomSheet, dialogBackground)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||
})
|
||||
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,10 @@ public class InputPanel extends LinearLayout
|
||||
private @Nullable Listener listener;
|
||||
private boolean emojiVisible;
|
||||
|
||||
private boolean hideForGroupState;
|
||||
private boolean hideForBlockedState;
|
||||
private boolean hideForSearch;
|
||||
|
||||
private ConversationStickerSuggestionAdapter stickerSuggestionAdapter;
|
||||
|
||||
public InputPanel(Context context) {
|
||||
@@ -317,6 +321,21 @@ public class InputPanel extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
public void setHideForGroupState(boolean hideForGroupState) {
|
||||
this.hideForGroupState = hideForGroupState;
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
public void setHideForBlockedState(boolean hideForBlockedState) {
|
||||
this.hideForBlockedState = hideForBlockedState;
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
public void setHideForSearch(boolean hideForSearch) {
|
||||
this.hideForSearch = hideForSearch;
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordPermissionRequired() {
|
||||
if (listener != null) listener.onRecorderPermissionRequired();
|
||||
@@ -495,6 +514,14 @@ public class InputPanel extends LinearLayout
|
||||
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
|
||||
}
|
||||
|
||||
private void updateVisibility() {
|
||||
if (hideForGroupState || hideForBlockedState || hideForSearch) {
|
||||
setVisibility(GONE);
|
||||
} else {
|
||||
setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener extends VoiceNoteDraftView.Listener {
|
||||
void onRecorderStarted();
|
||||
void onRecorderLocked();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Fullscreen Dialog Fragment which will dismiss itself when the keyboard is closed
|
||||
*/
|
||||
abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
DialogFragment(contentLayoutId),
|
||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||
KeyboardAwareLinearLayout.OnKeyboardHiddenListener {
|
||||
|
||||
private var hasShown = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setStyle(STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
|
||||
dialog.window?.setDimAmount(0f)
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
hasShown = false
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
return if (view is KeyboardAwareLinearLayout) {
|
||||
view.addOnKeyboardShownListener(this)
|
||||
view.addOnKeyboardHiddenListener(this)
|
||||
view
|
||||
} else {
|
||||
throw IllegalStateException("Expected parent of view hierarchy to be keyboard aware.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyboardShown() {
|
||||
hasShown = true
|
||||
}
|
||||
|
||||
override fun onKeyboardHidden() {
|
||||
if (hasShown) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,12 @@ public class ShapeScrim extends View {
|
||||
private final Paint eraser;
|
||||
private final ShapeType shape;
|
||||
private final float radius;
|
||||
private final int canvasColor;
|
||||
|
||||
private Bitmap scrim;
|
||||
private Canvas scrimCanvas;
|
||||
private int scrimWidth;
|
||||
private int scrimHeight;
|
||||
|
||||
public ShapeScrim(Context context) {
|
||||
this(context, null);
|
||||
@@ -57,13 +60,30 @@ public class ShapeScrim extends View {
|
||||
this.eraser = new Paint();
|
||||
this.eraser.setColor(0xFFFFFFFF);
|
||||
this.eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
this.canvasColor = Color.parseColor("#55BDBDBD");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
int shortDimension = Math.min(getWidth(), getHeight());
|
||||
float drawRadius = shortDimension * radius;
|
||||
|
||||
float left = (getMeasuredWidth() / 2 ) - drawRadius;
|
||||
float top = (getMeasuredHeight() / 2) - drawRadius;
|
||||
float right = left + (drawRadius * 2);
|
||||
float bottom = top + (drawRadius * 2);
|
||||
|
||||
scrimWidth = (int) (right - left);
|
||||
scrimHeight = (int) (bottom - top);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
int shortDimension = getWidth() < getHeight() ? getWidth() : getHeight();
|
||||
int shortDimension = Math.min(getWidth(), getHeight());
|
||||
float drawRadius = shortDimension * radius;
|
||||
|
||||
if (scrimCanvas == null) {
|
||||
@@ -72,7 +92,7 @@ public class ShapeScrim extends View {
|
||||
}
|
||||
|
||||
scrim.eraseColor(Color.TRANSPARENT);
|
||||
scrimCanvas.drawColor(Color.parseColor("#55BDBDBD"));
|
||||
scrimCanvas.drawColor(canvasColor);
|
||||
|
||||
if (shape == ShapeType.CIRCLE) drawCircle(scrimCanvas, drawRadius, eraser);
|
||||
else drawSquare(scrimCanvas, drawRadius, eraser);
|
||||
@@ -104,4 +124,12 @@ public class ShapeScrim extends View {
|
||||
|
||||
canvas.drawRoundRect(square, 25, 25, eraser);
|
||||
}
|
||||
|
||||
public int getScrimWidth() {
|
||||
return scrimWidth;
|
||||
}
|
||||
|
||||
public int getScrimHeight() {
|
||||
return scrimHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +143,9 @@ public class EmojiPageView extends RecyclerView implements VariationSelectorList
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
if (layoutManager instanceof GridLayoutManager) {
|
||||
int viewWidth = w - getPaddingStart() - getPaddingEnd();
|
||||
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
|
||||
int spanCount = Math.max(w / idealWidth, 1);
|
||||
int spanCount = Math.max(viewWidth / idealWidth, 1);
|
||||
|
||||
((GridLayoutManager) layoutManager).setSpanCount(spanCount);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.emoji.EmojiPageCache;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@@ -52,7 +53,7 @@ class EmojiProvider {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||
|
||||
for (EmojiParser.Candidate candidate : matches) {
|
||||
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo());
|
||||
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo(), tv::requestLayout);
|
||||
|
||||
if (drawable != null) {
|
||||
builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(),
|
||||
@@ -65,10 +66,17 @@ class EmojiProvider {
|
||||
|
||||
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji) {
|
||||
EmojiDrawInfo drawInfo = EmojiSource.getLatest().getEmojiTree().getEmoji(emoji, 0, emoji.length());
|
||||
return getEmojiDrawable(context, drawInfo);
|
||||
return getEmojiDrawable(context, drawInfo, null);
|
||||
}
|
||||
|
||||
private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo) {
|
||||
/**
|
||||
* Gets an EmojiDrawable from the Page Cache
|
||||
*
|
||||
* @param context Context object used in reading and writing from disk
|
||||
* @param drawInfo Information about the emoji being displayed
|
||||
* @param onEmojiLoaded Runnable which will trigger when an emoji is loaded from disk
|
||||
*/
|
||||
private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, @Nullable Runnable onEmojiLoaded) {
|
||||
if (drawInfo == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -77,19 +85,30 @@ class EmojiProvider {
|
||||
final EmojiSource source = EmojiSource.getLatest();
|
||||
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
|
||||
|
||||
EmojiPageCache.INSTANCE
|
||||
.load(context, drawInfo.getPage(), lowMemoryDecodeScale)
|
||||
.addListener(new FutureTaskListener<Bitmap>() {
|
||||
@Override
|
||||
public void onSuccess(Bitmap result) {
|
||||
ThreadUtil.runOnMain(() -> drawable.setBitmap(result));
|
||||
}
|
||||
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException exception) {
|
||||
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
|
||||
}
|
||||
});
|
||||
if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) {
|
||||
ThreadUtil.runOnMain(() -> drawable.setBitmap(((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap()));
|
||||
} else if (loadResult instanceof EmojiPageCache.LoadResult.Async) {
|
||||
((EmojiPageCache.LoadResult.Async) loadResult).getTask().addListener(new FutureTaskListener<Bitmap>() {
|
||||
@Override
|
||||
public void onSuccess(Bitmap result) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
drawable.setBitmap(result);
|
||||
if (onEmojiLoaded != null) {
|
||||
onEmojiLoaded.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException exception) {
|
||||
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
|
||||
}
|
||||
|
||||
return drawable;
|
||||
}
|
||||
@@ -122,10 +141,10 @@ class EmojiProvider {
|
||||
final int xStart = (index % emojiPerRow) * glyphWidth;
|
||||
final int yStart = (index / emojiPerRow) * glyphHeight;
|
||||
|
||||
this.emojiBounds = new Rect(xStart,
|
||||
yStart,
|
||||
xStart + glyphWidth,
|
||||
yStart + glyphHeight);
|
||||
this.emojiBounds = new Rect(xStart + 1,
|
||||
yStart + 1,
|
||||
xStart + glyphWidth - 1,
|
||||
yStart + glyphHeight - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,8 +5,11 @@ import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Annotation;
|
||||
import android.text.Layout;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextDirectionHeuristic;
|
||||
import android.text.TextDirectionHeuristics;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
@@ -33,19 +36,23 @@ import java.util.List;
|
||||
public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
private final boolean scaleEmojis;
|
||||
private final boolean forceCustom;
|
||||
|
||||
private static final char ELLIPSIS = '…';
|
||||
|
||||
private CharSequence previousText;
|
||||
private BufferType previousBufferType;
|
||||
private float originalFontSize;
|
||||
private boolean useSystemEmoji;
|
||||
private boolean sizeChangeInProgress;
|
||||
private int maxLength;
|
||||
private CharSequence overflowText;
|
||||
private CharSequence previousOverflowText;
|
||||
private boolean renderMentions;
|
||||
private boolean forceCustom;
|
||||
private CharSequence previousText;
|
||||
private BufferType previousBufferType;
|
||||
private float originalFontSize;
|
||||
private boolean useSystemEmoji;
|
||||
private boolean sizeChangeInProgress;
|
||||
private int maxLength;
|
||||
private CharSequence overflowText;
|
||||
private CharSequence previousOverflowText;
|
||||
private boolean renderMentions;
|
||||
private boolean measureLastLine;
|
||||
private int lastLineWidth = -1;
|
||||
private TextDirectionHeuristic textDirection;
|
||||
private boolean isJumbomoji;
|
||||
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
|
||||
@@ -61,10 +68,11 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
||||
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||
renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true);
|
||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
||||
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||
renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true);
|
||||
measureLastLine = a.getBoolean(R.styleable.EmojiTextView_measureLastLine, false);
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
|
||||
@@ -74,6 +82,8 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
if (renderMentions) {
|
||||
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.transparent_black_20));
|
||||
}
|
||||
|
||||
textDirection = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? TextDirectionHeuristics.FIRSTSTRONG_RTL : TextDirectionHeuristics.ANYRTL_LTR;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -103,8 +113,10 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
if (emojis <= 4) scale += 0.25f;
|
||||
if (emojis <= 2) scale += 0.25f;
|
||||
|
||||
isJumbomoji = scale > 1.0f;
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize * scale);
|
||||
} else if (scaleEmojis) {
|
||||
isJumbomoji = false;
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize);
|
||||
}
|
||||
|
||||
@@ -139,11 +151,50 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
CharSequence text = getText();
|
||||
if (getLayout() == null || !measureLastLine || text == null || text.length() == 0) {
|
||||
lastLineWidth = -1;
|
||||
} else {
|
||||
Layout layout = getLayout();
|
||||
int lines = layout.getLineCount();
|
||||
int start = layout.getLineStart(lines - 1);
|
||||
int count = text.length() - start;
|
||||
|
||||
if ((getLayoutDirection() == LAYOUT_DIRECTION_LTR && textDirection.isRtl(text, start, count)) ||
|
||||
(getLayoutDirection() == LAYOUT_DIRECTION_RTL && !textDirection.isRtl(text, start, count))) {
|
||||
lastLineWidth = getMeasuredWidth();
|
||||
} else {
|
||||
lastLineWidth = (int) getPaint().measureText(text, start, text.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getLastLineWidth() {
|
||||
return lastLineWidth;
|
||||
}
|
||||
|
||||
public boolean isSingleLine() {
|
||||
return getLayout() != null && getLayout().getLineCount() == 1;
|
||||
}
|
||||
|
||||
public boolean isJumbomoji() {
|
||||
return isJumbomoji;
|
||||
}
|
||||
|
||||
public void setOverflowText(@Nullable CharSequence overflowText) {
|
||||
this.overflowText = overflowText;
|
||||
setText(previousText, BufferType.SPANNABLE);
|
||||
}
|
||||
|
||||
public void setForceCustomEmoji(boolean forceCustom) {
|
||||
if (this.forceCustom != forceCustom) {
|
||||
this.forceCustom = forceCustom;
|
||||
setText(previousText, BufferType.SPANNABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void ellipsizeAnyTextForMaxLength() {
|
||||
if (maxLength > 0 && getText().length() > maxLength + 1) {
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -20,6 +21,8 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
private static final String TAG = Log.tag(MediaKeyboard.class);
|
||||
@@ -40,6 +43,10 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setFragmentManager(@NonNull FragmentManager fragmentManager) {
|
||||
this.fragmentManager = fragmentManager;
|
||||
}
|
||||
|
||||
public void setKeyboardListener(@Nullable MediaKeyboardListener listener) {
|
||||
this.keyboardListener = listener;
|
||||
}
|
||||
@@ -125,13 +132,32 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
private void initView() {
|
||||
if (!isInitialised) {
|
||||
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
|
||||
|
||||
if (fragmentManager == null) {
|
||||
FragmentActivity activity = resolveActivity(getContext());
|
||||
fragmentManager = activity.getSupportFragmentManager();
|
||||
}
|
||||
|
||||
keyboardPagerFragment = new KeyboardPagerFragment();
|
||||
fragmentManager.beginTransaction()
|
||||
.replace(R.id.media_keyboard_fragment_container, keyboardPagerFragment)
|
||||
.commitNowAllowingStateLoss();
|
||||
|
||||
keyboardState = State.NORMAL;
|
||||
latestKeyboardHeight = -1;
|
||||
isInitialised = true;
|
||||
fragmentManager = ((FragmentActivity) getContext()).getSupportFragmentManager();
|
||||
keyboardPagerFragment = (KeyboardPagerFragment) fragmentManager.findFragmentById(R.id.media_keyboard_fragment_container);
|
||||
}
|
||||
}
|
||||
|
||||
private static FragmentActivity resolveActivity(@Nullable Context context) {
|
||||
if (context instanceof FragmentActivity) {
|
||||
return (FragmentActivity) context;
|
||||
} else if (context instanceof ContextThemeWrapper) {
|
||||
return resolveActivity(((ContextThemeWrapper) context).getBaseContext());
|
||||
} else {
|
||||
throw new IllegalStateException("Could not locate FragmentActivity");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
@@ -40,12 +42,12 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
|
||||
identityStore.setApproval(identityRecord.getRecipientId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.util.List;
|
||||
@@ -39,27 +41,16 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
}
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
resendListener.onResendMessage();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
return null;
|
||||
}, nothing -> resendListener.onResendMessage());
|
||||
}
|
||||
|
||||
public interface ResendListener {
|
||||
|
||||
@@ -14,6 +14,11 @@ public class SmoothScrollingLinearLayoutManager extends LinearLayoutManager {
|
||||
super(context, RecyclerView.VERTICAL, reverseLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPredictiveItemAnimations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void smoothScrollToPosition(@NonNull Context context, int position, float millisecondsPerInch) {
|
||||
final LinearSmoothScroller scroller = new LinearSmoothScroller(context) {
|
||||
@Override
|
||||
|
||||
@@ -43,6 +43,11 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshState()
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
viewModel = ViewModelProviders.of(this)[AccountSettingsViewModel::class.java]
|
||||
|
||||
@@ -70,7 +75,8 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_app_protection__pin_reminders),
|
||||
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__youll_be_asked_less_frequently),
|
||||
isChecked = state.pinRemindersEnabled,
|
||||
isChecked = state.hasPin && state.pinRemindersEnabled,
|
||||
isEnabled = state.hasPin,
|
||||
onClick = {
|
||||
setPinRemindersEnabled(!state.pinRemindersEnabled)
|
||||
}
|
||||
@@ -80,6 +86,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
title = DSLSettingsText.from(R.string.preferences_app_protection__registration_lock),
|
||||
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__require_your_signal_pin),
|
||||
isChecked = state.registrationLockEnabled,
|
||||
isEnabled = state.hasPin,
|
||||
onClick = {
|
||||
setRegistrationLockEnabled(!state.registrationLockEnabled)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
@@ -21,7 +22,7 @@ class ChatsSettingsViewModel(private val repository: ChatsSettingsRepository) :
|
||||
useAddressBook = SignalStore.settings().isPreferSystemContactPhotos,
|
||||
useSystemEmoji = SignalStore.settings().isPreferSystemEmoji,
|
||||
enterKeySends = SignalStore.settings().isEnterKeySends,
|
||||
chatBackupsEnabled = SignalStore.settings().isBackupEnabled
|
||||
chatBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
@@ -73,8 +74,8 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_refresh_remote_values),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_refresh_remote_values_description),
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_refresh_remote_config),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_refresh_remote_config_description),
|
||||
onClick = {
|
||||
refreshRemoteValues()
|
||||
}
|
||||
@@ -82,7 +83,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_display)
|
||||
sectionHeaderPref(R.string.preferences__internal_misc)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_user_details),
|
||||
@@ -93,6 +94,15 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_shake_to_report),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_shake_to_report_description),
|
||||
isChecked = state.shakeToReport,
|
||||
onClick = {
|
||||
viewModel.setShakeToReport(!state.shakeToReport)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_storage_service)
|
||||
@@ -255,6 +265,18 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_local_metrics)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_clear_local_metrics),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_clear_all_local_metrics_state),
|
||||
onClick = {
|
||||
clearAllLocalMetricsState()
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_calling)
|
||||
|
||||
radioPref(
|
||||
@@ -354,4 +376,9 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key shared state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun clearAllLocalMetricsState() {
|
||||
LocalMetricsDatabase.getInstance(ApplicationDependencies.getApplication()).clear()
|
||||
Toast.makeText(context, "Cleared all local metrics state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.thoughtcrime.securesms.emoji.EmojiFiles
|
||||
|
||||
data class InternalSettingsState(
|
||||
val seeMoreUserDetails: Boolean,
|
||||
val shakeToReport: Boolean,
|
||||
val gv2doNotCreateGv2Groups: Boolean,
|
||||
val gv2forceInvites: Boolean,
|
||||
val gv2ignoreServerChanges: Boolean,
|
||||
|
||||
@@ -25,6 +25,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setShakeToReport(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.SHAKE_TO_REPORT, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setGv2DoNotCreateGv2Groups(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.GV2_DO_NOT_CREATE_GV2, enabled)
|
||||
refresh()
|
||||
@@ -86,6 +91,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
|
||||
private fun getState() = InternalSettingsState(
|
||||
seeMoreUserDetails = SignalStore.internalValues().recipientDetails(),
|
||||
shakeToReport = SignalStore.internalValues().shakeToReport(),
|
||||
gv2doNotCreateGv2Groups = SignalStore.internalValues().gv2DoNotCreateGv2Groups(),
|
||||
gv2forceInvites = SignalStore.internalValues().gv2ForceInvites(),
|
||||
gv2ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(),
|
||||
|
||||
@@ -38,7 +38,7 @@ class ExpireTimerSettingsRepository(val context: Context) {
|
||||
} else {
|
||||
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipientId, newExpirationTime)
|
||||
val outgoingMessage = OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
||||
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), false, null)
|
||||
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), false, null, null)
|
||||
consumer.invoke(Result.success(newExpirationTime))
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,6 @@ class ExpireTimerSettingsRepository(val context: Context) {
|
||||
private fun getThreadId(recipientId: RecipientId): Long {
|
||||
val threadDatabase: ThreadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||
return threadDatabase.getThreadIdFor(recipient)
|
||||
return threadDatabase.getOrCreateThreadIdFor(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class ExpireTimerSettingsViewModel(val config: Config, private val repository: E
|
||||
|
||||
init {
|
||||
if (recipientId != null) {
|
||||
store.update(Recipient.live(recipientId).liveData) { r, s -> s.copy(initialTimer = r.expireMessages, isForRecipient = true) }
|
||||
store.update(Recipient.live(recipientId).liveData) { r, s -> s.copy(initialTimer = r.expiresInSeconds, isForRecipient = true) }
|
||||
} else {
|
||||
store.update { it.copy(initialTimer = config.initialValue ?: SignalStore.settings().universalExpireTimer) }
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ sealed class ConversationSettingsEvent {
|
||||
val groupId: GroupId,
|
||||
val selectionWarning: Int,
|
||||
val selectionLimit: Int,
|
||||
val isAnnouncementGroup: Boolean,
|
||||
val groupMembersWithoutSelf: List<RecipientId>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
|
||||
@@ -237,11 +237,13 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
AvatarPreference.Model(
|
||||
recipient = state.recipient,
|
||||
onAvatarClick = { avatar ->
|
||||
requireActivity().apply {
|
||||
startActivity(
|
||||
AvatarPreviewActivity.intentFromRecipientId(this, state.recipient.id),
|
||||
AvatarPreviewActivity.createTransitionBundle(this, avatar)
|
||||
)
|
||||
if (!state.recipient.isSelf) {
|
||||
requireActivity().apply {
|
||||
startActivity(
|
||||
AvatarPreviewActivity.intentFromRecipientId(this, state.recipient.id),
|
||||
AvatarPreviewActivity.createTransitionBundle(this, avatar)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -315,7 +317,15 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
ButtonStripPreference.Model(
|
||||
state = state.buttonStripState,
|
||||
onVideoClick = {
|
||||
CommunicationActions.startVideoCall(requireActivity(), state.recipient)
|
||||
if (state.recipient.isPushV2Group && state.requireGroupSettingsState().isAnnouncementGroup && !state.requireGroupSettingsState().isSelfAdmin) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ConversationActivity_cant_start_group_call)
|
||||
.setMessage(R.string.ConversationActivity_only_admins_of_this_group_can_start_a_call)
|
||||
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
|
||||
.show()
|
||||
} else {
|
||||
CommunicationActions.startVideoCall(requireActivity(), state.recipient)
|
||||
}
|
||||
},
|
||||
onAudioClick = {
|
||||
CommunicationActions.startVoiceCall(requireActivity(), state.recipient)
|
||||
@@ -664,6 +674,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
ContactsCursorLoader.DisplayMode.FLAG_PUSH,
|
||||
addMembersToGroup.selectionWarning,
|
||||
addMembersToGroup.selectionLimit,
|
||||
addMembersToGroup.isAnnouncementGroup,
|
||||
addMembersToGroup.groupMembersWithoutSelf
|
||||
),
|
||||
REQUEST_CODE_ADD_MEMBERS_TO_GROUP
|
||||
|
||||
@@ -11,13 +11,15 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -26,6 +28,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private val TAG = Log.tag(ConversationSettingsRepository::class.java)
|
||||
|
||||
@@ -61,13 +64,9 @@ class ConversationSettingsRepository(
|
||||
SignalExecutors.BOUNDED.execute { consumer(DatabaseFactory.getGroupDatabase(context).activeGroupCount > 0) }
|
||||
}
|
||||
|
||||
fun getIdentity(recipientId: RecipientId, consumer: (IdentityDatabase.IdentityRecord?) -> Unit) {
|
||||
fun getIdentity(recipientId: RecipientId, consumer: (IdentityRecord?) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(
|
||||
DatabaseFactory.getIdentityDatabase(context)
|
||||
.getIdentity(recipientId)
|
||||
.orNull()
|
||||
)
|
||||
consumer(ApplicationDependencies.getIdentityStore().getIdentityRecord(recipientId).orNull())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +129,9 @@ class ConversationSettingsRepository(
|
||||
members.addAll(groupRecord.members)
|
||||
members.addAll(pendingMembers)
|
||||
|
||||
GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits())
|
||||
GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits(), groupRecord.isAnnouncementGroup)
|
||||
} else {
|
||||
GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits())
|
||||
GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits(), false)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -140,6 +139,25 @@ class ConversationSettingsRepository(
|
||||
|
||||
fun addMembers(groupId: GroupId, selected: List<RecipientId>, consumer: (GroupAddMembersResult) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val record: GroupDatabase.GroupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get()
|
||||
|
||||
if (record.isAnnouncementGroup) {
|
||||
val needsResolve = selected
|
||||
.map { Recipient.resolved(it) }
|
||||
.filter { it.announcementGroupCapability != Recipient.Capability.SUPPORTED && !it.isSelf }
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
|
||||
ApplicationDependencies.getJobManager().runSynchronously(RetrieveProfileJob(needsResolve), TimeUnit.SECONDS.toMillis(10))
|
||||
|
||||
val updatedWithCapabilities = needsResolve.map { Recipient.resolved(it) }
|
||||
|
||||
if (updatedWithCapabilities.any { it.announcementGroupCapability != Recipient.Capability.SUPPORTED }) {
|
||||
consumer(GroupAddMembersResult.Failure(GroupChangeFailureReason.NOT_ANNOUNCEMENT_CAPABLE))
|
||||
return@execute
|
||||
}
|
||||
}
|
||||
|
||||
consumer(
|
||||
try {
|
||||
val groupActionResult = GroupManager.addMembers(context, groupId.requirePush(), selected)
|
||||
@@ -198,7 +216,14 @@ class ConversationSettingsRepository(
|
||||
Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!")
|
||||
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipientId)
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
|
||||
if (recipient.hasUuid()) {
|
||||
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipient.requireUuid().toString())
|
||||
}
|
||||
if (recipient.hasE164()) {
|
||||
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipient.requireE164())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.settings.conversation
|
||||
import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -43,7 +43,7 @@ sealed class SpecificSettingsState {
|
||||
abstract val isLoaded: Boolean
|
||||
|
||||
data class RecipientSettingsState(
|
||||
val identityRecord: IdentityDatabase.IdentityRecord? = null,
|
||||
val identityRecord: IdentityRecord? = null,
|
||||
val allGroupsInCommon: List<Recipient> = listOf(),
|
||||
val groupsInCommon: List<Recipient> = listOf(),
|
||||
val selfHasGroups: Boolean = false,
|
||||
@@ -75,7 +75,8 @@ sealed class SpecificSettingsState {
|
||||
private val groupDescriptionLoaded: Boolean = false,
|
||||
val groupLinkEnabled: Boolean = false,
|
||||
val membershipCountDescription: String = "",
|
||||
val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE
|
||||
val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE,
|
||||
val isAnnouncementGroup: Boolean = false
|
||||
) : SpecificSettingsState() {
|
||||
|
||||
override val isLoaded: Boolean = groupTitleLoaded && groupDescriptionLoaded
|
||||
|
||||
@@ -144,7 +144,7 @@ sealed class ConversationSettingsViewModel(
|
||||
isMuteAvailable = !recipient.isSelf,
|
||||
isSearchAvailable = true
|
||||
),
|
||||
disappearingMessagesLifespan = recipient.expireMessages,
|
||||
disappearingMessagesLifespan = recipient.expiresInSeconds,
|
||||
canModifyBlockedState = !recipient.isSelf,
|
||||
specificSettingsState = state.requireRecipientSettingsState().copy(
|
||||
contactLinkState = when {
|
||||
@@ -322,6 +322,14 @@ sealed class ConversationSettingsViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.isAnnouncementGroup) { announcementGroup, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
isAnnouncementGroup = announcementGroup
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val isMessageRequestAccepted: LiveData<Boolean> = LiveDataUtil.mapAsync(liveGroup.groupRecipient) { r -> repository.isMessageRequestAccepted(r) }
|
||||
val descriptionState: LiveData<DescriptionState> = LiveDataUtil.combineLatest(liveGroup.description, isMessageRequestAccepted, ::DescriptionState)
|
||||
|
||||
@@ -386,11 +394,13 @@ sealed class ConversationSettingsViewModel(
|
||||
override fun onAddToGroup() {
|
||||
repository.getGroupCapacity(groupId) { capacityResult ->
|
||||
if (capacityResult.getRemainingCapacity() > 0) {
|
||||
|
||||
internalEvents.postValue(
|
||||
ConversationSettingsEvent.AddMembersToGroup(
|
||||
groupId,
|
||||
capacityResult.getSelectionWarning(),
|
||||
capacityResult.getSelectionLimit(),
|
||||
capacityResult.isAnnouncementGroup,
|
||||
capacityResult.getMembersWithoutSelf()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -7,7 +7,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
class GroupCapacityResult(
|
||||
private val selfId: RecipientId,
|
||||
private val members: List<RecipientId>,
|
||||
private val selectionLimits: SelectionLimits
|
||||
private val selectionLimits: SelectionLimits,
|
||||
val isAnnouncementGroup: Boolean
|
||||
) {
|
||||
fun getMembers(): List<RecipientId?> {
|
||||
return members
|
||||
|
||||
@@ -72,6 +72,20 @@ class PermissionsSettingsFragment : DSLSettingsFragment(
|
||||
viewModel.setNonAdminCanEditGroupInfo(it == 1)
|
||||
}
|
||||
)
|
||||
|
||||
if (state.announcementGroupPermissionEnabled) {
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.PermissionsSettingsFragment__send_messages),
|
||||
isEnabled = state.selfCanEditSettings,
|
||||
listItems = permissionsOptions,
|
||||
dialogTitle = DSLSettingsText.from(R.string.PermissionsSettingsFragment__who_can_send_messages),
|
||||
selected = getSelected(!state.announcementGroup),
|
||||
confirmAction = true,
|
||||
onSelected = {
|
||||
viewModel.setAnnouncementGroup(it == 0)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,4 +42,18 @@ class PermissionsSettingsRepository(private val context: Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyAnnouncementGroupChange(groupId: GroupId, isAnnouncementGroup: Boolean, error: GroupChangeErrorCallback) {
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
try {
|
||||
GroupManager.applyAnnouncementGroupChange(context, groupId.requireV2(), isAnnouncementGroup)
|
||||
} catch (e: GroupChangeException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,7 @@ package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
data class PermissionsSettingsState(
|
||||
val selfCanEditSettings: Boolean = false,
|
||||
val nonAdminCanAddMembers: Boolean = false,
|
||||
val nonAdminCanEditGroupInfo: Boolean = false
|
||||
val nonAdminCanEditGroupInfo: Boolean = false,
|
||||
val announcementGroupPermissionEnabled: Boolean = false,
|
||||
val announcementGroup: Boolean = false
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
@@ -33,6 +34,18 @@ class PermissionsSettingsViewModel(
|
||||
store.update(liveGroup.attributesAccessControl) { attributesAccessControl, state ->
|
||||
state.copy(nonAdminCanEditGroupInfo = attributesAccessControl == GroupAccessControl.ALL_MEMBERS)
|
||||
}
|
||||
|
||||
store.update(liveGroup.isAnnouncementGroup) { isAnnouncementGroup, state ->
|
||||
state.copy(
|
||||
announcementGroup = isAnnouncementGroup,
|
||||
announcementGroupPermissionEnabled = state.announcementGroupPermissionEnabled || isAnnouncementGroup
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.groupRecipient) { groupRecipient, state ->
|
||||
val allHaveCapability = groupRecipient.participants.map { it.announcementGroupCapability }.all { it == Recipient.Capability.SUPPORTED }
|
||||
state.copy(announcementGroupPermissionEnabled = allHaveCapability || state.announcementGroup)
|
||||
}
|
||||
}
|
||||
|
||||
fun setNonAdminCanAddMembers(nonAdminCanAddMembers: Boolean) {
|
||||
@@ -47,6 +60,12 @@ class PermissionsSettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setAnnouncementGroup(announcementGroup: Boolean) {
|
||||
repository.applyAnnouncementGroupChange(groupId, announcementGroup) { reason ->
|
||||
internalEvents.postValue(PermissionsSettingsEvents.GroupChangeError(reason))
|
||||
}
|
||||
}
|
||||
|
||||
private fun Boolean.asGroupAccessControl(): GroupAccessControl {
|
||||
return if (this) {
|
||||
GroupAccessControl.ALL_MEMBERS
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds
|
||||
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -13,7 +14,6 @@ import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment
|
||||
|
||||
class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ConversationSettingsFragment__sounds_and_notifications
|
||||
@@ -110,7 +110,8 @@ class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
|
||||
summary = DSLSettingsText.from(customSoundSummary),
|
||||
onClick = {
|
||||
CustomNotificationsDialogFragment.create(state.recipientId).show(parentFragmentManager, null)
|
||||
val action = SoundsAndNotificationsSettingsFragmentDirections.actionSoundsAndNotificationsSettingsFragmentToCustomNotificationsSettingsFragment(state.recipientId)
|
||||
Navigation.findNavController(requireView()).navigate(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds.custom
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.util.RingtoneUtil
|
||||
|
||||
class CustomNotificationsSettingsFragment : DSLSettingsFragment(R.string.CustomNotificationsDialogFragment__custom_notifications) {
|
||||
|
||||
private val vibrateLabels: Array<String> by lazy {
|
||||
resources.getStringArray(R.array.recipient_vibrate_entries)
|
||||
}
|
||||
|
||||
private val viewModel: CustomNotificationsSettingsViewModel by viewModels(factoryProducer = this::createFactory)
|
||||
|
||||
private lateinit var callSoundResultLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var messageSoundResultLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private fun createFactory(): CustomNotificationsSettingsViewModel.Factory {
|
||||
val recipientId = CustomNotificationsSettingsFragmentArgs.fromBundle(requireArguments()).recipientId
|
||||
val repository = CustomNotificationsSettingsRepository(requireContext())
|
||||
|
||||
return CustomNotificationsSettingsViewModel.Factory(recipientId, repository)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
messageSoundResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
handleResult(result, viewModel::setMessageSound)
|
||||
}
|
||||
|
||||
callSoundResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
handleResult(result, viewModel::setCallSound)
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResult(result: ActivityResult, resultHandler: (Uri?) -> Unit) {
|
||||
val resultCode = result.resultCode
|
||||
val data = result.data
|
||||
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
val uri: Uri? = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||
resultHandler(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: CustomNotificationsSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
|
||||
sectionHeaderPref(R.string.CustomNotificationsDialogFragment__messages)
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__use_custom_notifications),
|
||||
isEnabled = state.isInitialLoadComplete,
|
||||
isChecked = state.hasCustomNotifications,
|
||||
onClick = { viewModel.setHasCustomNotifications(!state.hasCustomNotifications) }
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__notification_sound),
|
||||
summary = DSLSettingsText.from(getRingtoneSummary(requireContext(), state.messageSound, Settings.System.DEFAULT_NOTIFICATION_URI)),
|
||||
isEnabled = state.controlsEnabled,
|
||||
onClick = { requestSound(state.messageSound, false) }
|
||||
)
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__vibrate),
|
||||
isEnabled = state.controlsEnabled,
|
||||
isChecked = state.messageVibrateEnabled,
|
||||
onClick = { viewModel.setMessageVibrate(RecipientDatabase.VibrateState.fromBoolean(!state.messageVibrateEnabled)) }
|
||||
)
|
||||
} else {
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__vibrate),
|
||||
isEnabled = state.controlsEnabled,
|
||||
listItems = vibrateLabels,
|
||||
selected = state.messageVibrateState.id,
|
||||
onSelected = {
|
||||
viewModel.setMessageVibrate(RecipientDatabase.VibrateState.fromId(it))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (state.showCallingOptions) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.CustomNotificationsDialogFragment__call_settings)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__ringtone),
|
||||
summary = DSLSettingsText.from(getRingtoneSummary(requireContext(), state.callSound, Settings.System.DEFAULT_RINGTONE_URI)),
|
||||
isEnabled = state.controlsEnabled,
|
||||
onClick = { requestSound(state.callSound, true) }
|
||||
)
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__vibrate),
|
||||
isEnabled = state.controlsEnabled,
|
||||
listItems = vibrateLabels,
|
||||
selected = state.callVibrateState.id,
|
||||
onSelected = {
|
||||
viewModel.setCallVibrate(RecipientDatabase.VibrateState.fromId(it))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRingtoneSummary(context: Context, ringtone: Uri?, defaultNotificationUri: Uri?): String {
|
||||
if (ringtone == null || ringtone == defaultNotificationUri) {
|
||||
return context.getString(R.string.CustomNotificationsDialogFragment__default)
|
||||
} else if (ringtone.toString().isEmpty()) {
|
||||
return context.getString(R.string.preferences__silent)
|
||||
} else {
|
||||
val tone = RingtoneUtil.getRingtone(requireContext(), ringtone)
|
||||
if (tone != null) {
|
||||
return tone.getTitle(context)
|
||||
}
|
||||
}
|
||||
return context.getString(R.string.CustomNotificationsDialogFragment__default)
|
||||
}
|
||||
|
||||
private fun requestSound(current: Uri?, forCalls: Boolean) {
|
||||
val existing: Uri? = when {
|
||||
current == null -> getDefaultSound(forCalls)
|
||||
current.toString().isEmpty() -> null
|
||||
else -> current
|
||||
}
|
||||
|
||||
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
|
||||
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
|
||||
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
|
||||
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, if (forCalls) RingtoneManager.TYPE_RINGTONE else RingtoneManager.TYPE_NOTIFICATION)
|
||||
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existing)
|
||||
}
|
||||
|
||||
if (forCalls) {
|
||||
callSoundResultLauncher.launch(intent)
|
||||
} else {
|
||||
messageSoundResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDefaultSound(forCalls: Boolean) = if (forCalls) Settings.System.DEFAULT_RINGTONE_URI else Settings.System.DEFAULT_NOTIFICATION_URI
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds.custom
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor
|
||||
|
||||
class CustomNotificationsSettingsRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
private val executor = SerialExecutor(SignalExecutors.BOUNDED)
|
||||
|
||||
fun initialize(recipientId: RecipientId, onInitializationComplete: () -> Unit) {
|
||||
executor.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
||||
|
||||
if (NotificationChannels.supported() && recipient.notificationChannel != null) {
|
||||
database.setMessageRingtone(recipient.id, NotificationChannels.getMessageRingtone(context, recipient))
|
||||
database.setMessageVibrate(recipient.id, RecipientDatabase.VibrateState.fromBoolean(NotificationChannels.getMessageVibrate(context, recipient)))
|
||||
|
||||
NotificationChannels.ensureCustomChannelConsistency(context)
|
||||
}
|
||||
|
||||
onInitializationComplete()
|
||||
}
|
||||
}
|
||||
|
||||
fun setHasCustomNotifications(recipientId: RecipientId, hasCustomNotifications: Boolean) {
|
||||
executor.execute {
|
||||
if (hasCustomNotifications) {
|
||||
createCustomNotificationChannel(recipientId)
|
||||
} else {
|
||||
deleteCustomNotificationChannel(recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMessageVibrate(recipientId: RecipientId, vibrateState: RecipientDatabase.VibrateState) {
|
||||
executor.execute {
|
||||
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setMessageVibrate(recipient.id, vibrateState)
|
||||
NotificationChannels.updateMessageVibrate(context, recipient, vibrateState)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCallingVibrate(recipientId: RecipientId, vibrateState: RecipientDatabase.VibrateState) {
|
||||
executor.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).setCallVibrate(recipientId, vibrateState)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMessageSound(recipientId: RecipientId, sound: Uri?) {
|
||||
executor.execute {
|
||||
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||
val defaultValue = SignalStore.settings().messageNotificationSound
|
||||
val newValue: Uri? = if (defaultValue == sound) null else sound ?: Uri.EMPTY
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.id, newValue)
|
||||
NotificationChannels.updateMessageRingtone(context, recipient, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCallSound(recipientId: RecipientId, sound: Uri?) {
|
||||
executor.execute {
|
||||
val defaultValue = SignalStore.settings().callRingtone
|
||||
val newValue: Uri? = if (defaultValue == sound) null else sound ?: Uri.EMPTY
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setCallRingtone(recipientId, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun createCustomNotificationChannel(recipientId: RecipientId) {
|
||||
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||
val channelId = NotificationChannels.createChannelFor(context, recipient)
|
||||
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.id, channelId)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun deleteCustomNotificationChannel(recipientId: RecipientId) {
|
||||
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.id, null)
|
||||
NotificationChannels.deleteChannelFor(context, recipient)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds.custom
|
||||
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
|
||||
data class CustomNotificationsSettingsState(
|
||||
val isInitialLoadComplete: Boolean = false,
|
||||
val hasCustomNotifications: Boolean = false,
|
||||
val controlsEnabled: Boolean = false,
|
||||
val messageVibrateState: RecipientDatabase.VibrateState = RecipientDatabase.VibrateState.DEFAULT,
|
||||
val messageVibrateEnabled: Boolean = false,
|
||||
val messageSound: Uri? = null,
|
||||
val callVibrateState: RecipientDatabase.VibrateState = RecipientDatabase.VibrateState.DEFAULT,
|
||||
val callSound: Uri? = null,
|
||||
val showCallingOptions: Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds.custom
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class CustomNotificationsSettingsViewModel(
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: CustomNotificationsSettingsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(CustomNotificationsSettingsState())
|
||||
|
||||
val state: LiveData<CustomNotificationsSettingsState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
repository.initialize(recipientId) {
|
||||
store.update {
|
||||
it.copy(
|
||||
isInitialLoadComplete = true,
|
||||
controlsEnabled = (!NotificationChannels.supported() || it.hasCustomNotifications)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
store.update(Recipient.live(recipientId).liveData) { recipient, state ->
|
||||
val recipientHasCustomNotifications = NotificationChannels.supported() && recipient.notificationChannel != null
|
||||
state.copy(
|
||||
hasCustomNotifications = recipientHasCustomNotifications,
|
||||
controlsEnabled = (!NotificationChannels.supported() || recipientHasCustomNotifications) && state.isInitialLoadComplete,
|
||||
messageSound = recipient.messageRingtone,
|
||||
messageVibrateState = recipient.messageVibrate,
|
||||
messageVibrateEnabled = when (recipient.messageVibrate) {
|
||||
RecipientDatabase.VibrateState.DEFAULT -> SignalStore.settings().isMessageVibrateEnabled
|
||||
RecipientDatabase.VibrateState.ENABLED -> true
|
||||
RecipientDatabase.VibrateState.DISABLED -> false
|
||||
},
|
||||
showCallingOptions = !recipient.isGroup && recipient.isRegistered,
|
||||
callSound = recipient.callRingtone,
|
||||
callVibrateState = recipient.callVibrate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setHasCustomNotifications(hasCustomNotifications: Boolean) {
|
||||
repository.setHasCustomNotifications(recipientId, hasCustomNotifications)
|
||||
}
|
||||
|
||||
fun setMessageVibrate(messageVibrateState: RecipientDatabase.VibrateState) {
|
||||
repository.setMessageVibrate(recipientId, messageVibrateState)
|
||||
}
|
||||
|
||||
fun setMessageSound(uri: Uri?) {
|
||||
repository.setMessageSound(recipientId, uri)
|
||||
}
|
||||
|
||||
fun setCallVibrate(callVibrateState: RecipientDatabase.VibrateState) {
|
||||
repository.setCallingVibrate(recipientId, callVibrateState)
|
||||
}
|
||||
|
||||
fun setCallSound(uri: Uri?) {
|
||||
repository.setCallSound(recipientId, uri)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: CustomNotificationsSettingsRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(CustomNotificationsSettingsViewModel(recipientId, repository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,12 @@ package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.media.AudioManager;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
@@ -45,9 +42,9 @@ import java.util.Objects;
|
||||
*/
|
||||
public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
|
||||
public static final String EXTRA_THREAD_ID = "voice.note.thread_id";
|
||||
public static final String EXTRA_MESSAGE_ID = "voice.note.message_id";
|
||||
public static final String EXTRA_PROGRESS = "voice.note.playhead";
|
||||
public static final String EXTRA_THREAD_ID = "voice.note.thread_id";
|
||||
public static final String EXTRA_MESSAGE_ID = "voice.note.message_id";
|
||||
public static final String EXTRA_PROGRESS = "voice.note.playhead";
|
||||
public static final String EXTRA_PLAY_SINGLE = "voice.note.play.single";
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteMediaController.class);
|
||||
@@ -57,6 +54,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
private ProgressEventHandler progressEventHandler;
|
||||
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
|
||||
private LiveData<Optional<VoiceNotePlayerView.State>> voiceNotePlayerViewState;
|
||||
private VoiceNoteProximityWakeLockManager voiceNoteProximityWakeLockManager;
|
||||
|
||||
private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback();
|
||||
|
||||
@@ -76,7 +74,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
LiveRecipient threadRecipient = Recipient.live(message.getThreadRecipientId());
|
||||
LiveData<String> name = LiveDataUtil.combineLatest(sender.getLiveDataResolved(),
|
||||
threadRecipient.getLiveDataResolved(),
|
||||
(s, t) -> VoiceNoteMediaDescriptionCompatFactory.getTitle(activity, s, t, null));
|
||||
(s, t) -> VoiceNoteMediaItemFactory.getTitle(activity, s, t, null));
|
||||
|
||||
return Transformations.map(name, displayName -> Optional.of(
|
||||
new VoiceNotePlayerView.State(
|
||||
@@ -108,7 +106,9 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
mediaBrowser.connect();
|
||||
if (!mediaBrowser.isConnected()) {
|
||||
mediaBrowser.connect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,6 +123,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
if (MediaControllerCompat.getMediaController(activity) != null) {
|
||||
MediaControllerCompat.getMediaController(activity).unregisterCallback(mediaControllerCompatCallback);
|
||||
}
|
||||
|
||||
mediaBrowser.disconnect();
|
||||
}
|
||||
|
||||
@@ -258,35 +259,54 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
private final class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
|
||||
@Override
|
||||
public void onConnected() {
|
||||
try {
|
||||
MediaSessionCompat.Token token = mediaBrowser.getSessionToken();
|
||||
MediaControllerCompat mediaController = new MediaControllerCompat(activity, token);
|
||||
MediaSessionCompat.Token token = mediaBrowser.getSessionToken();
|
||||
MediaControllerCompat mediaController = new MediaControllerCompat(activity, token);
|
||||
|
||||
MediaControllerCompat.setMediaController(activity, mediaController);
|
||||
MediaControllerCompat.setMediaController(activity, mediaController);
|
||||
|
||||
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
|
||||
if (canExtractPlaybackInformationFromMetadata(mediaMetadataCompat)) {
|
||||
VoiceNotePlaybackState newState = extractStateFromMetadata(mediaController, mediaMetadataCompat, null);
|
||||
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
|
||||
if (canExtractPlaybackInformationFromMetadata(mediaMetadataCompat)) {
|
||||
VoiceNotePlaybackState newState = extractStateFromMetadata(mediaController, mediaMetadataCompat, null);
|
||||
|
||||
if (newState != null) {
|
||||
voiceNotePlaybackState.postValue(newState);
|
||||
} else {
|
||||
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
|
||||
}
|
||||
if (newState != null) {
|
||||
voiceNotePlaybackState.postValue(newState);
|
||||
} else {
|
||||
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
mediaController.registerCallback(mediaControllerCompatCallback);
|
||||
cleanUpOldProximityWakeLockManager();
|
||||
voiceNoteProximityWakeLockManager = new VoiceNoteProximityWakeLockManager(activity, mediaController);
|
||||
|
||||
mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState());
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "onConnected: Failed to set media controller", e);
|
||||
mediaController.registerCallback(mediaControllerCompatCallback);
|
||||
|
||||
mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionSuspended() {
|
||||
Log.d(TAG, "Voice note MediaBrowser connection suspended.");
|
||||
cleanUpOldProximityWakeLockManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailed() {
|
||||
Log.d(TAG, "Voice note MediaBrowser connection failed.");
|
||||
cleanUpOldProximityWakeLockManager();
|
||||
}
|
||||
|
||||
private void cleanUpOldProximityWakeLockManager() {
|
||||
if (voiceNoteProximityWakeLockManager != null) {
|
||||
Log.d(TAG, "Session reconnected, cleaning up old wake lock manager");
|
||||
voiceNoteProximityWakeLockManager.unregisterCallbacksAndRelease();
|
||||
voiceNoteProximityWakeLockManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean canExtractPlaybackInformationFromMetadata(@Nullable MediaMetadataCompat mediaMetadataCompat) {
|
||||
return mediaMetadataCompat != null &&
|
||||
mediaMetadataCompat.getDescription() != null &&
|
||||
return mediaMetadataCompat != null &&
|
||||
mediaMetadataCompat.getDescription() != null &&
|
||||
mediaMetadataCompat.getDescription().getMediaUri() != null;
|
||||
}
|
||||
|
||||
@@ -295,7 +315,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
@Nullable VoiceNotePlaybackState previousState)
|
||||
{
|
||||
Uri mediaUri = Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri());
|
||||
boolean autoReset = Objects.equals(mediaUri, VoiceNotePlaybackPreparer.NEXT_URI) || Objects.equals(mediaUri, VoiceNotePlaybackPreparer.END_URI);
|
||||
boolean autoReset = Objects.equals(mediaUri, VoiceNoteMediaItemFactory.NEXT_URI) || Objects.equals(mediaUri, VoiceNoteMediaItemFactory.END_URI);
|
||||
long position = mediaController.getPlaybackState().getPosition();
|
||||
long duration = mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||
Bundle extras = mediaController.getExtras();
|
||||
@@ -357,17 +377,17 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
long timestamp = -1L;
|
||||
|
||||
if (mediaExtras != null) {
|
||||
messageId = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID, -1L);
|
||||
messagePosition = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION, -1L);
|
||||
threadId = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID, -1L);
|
||||
timestamp = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_TIMESTAMP, -1L);
|
||||
messageId = mediaExtras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID, -1L);
|
||||
messagePosition = mediaExtras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_POSITION, -1L);
|
||||
threadId = mediaExtras.getLong(VoiceNoteMediaItemFactory.EXTRA_THREAD_ID, -1L);
|
||||
timestamp = mediaExtras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_TIMESTAMP, -1L);
|
||||
|
||||
String serializedSenderId = mediaExtras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID);
|
||||
String serializedSenderId = mediaExtras.getString(VoiceNoteMediaItemFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID);
|
||||
if (serializedSenderId != null) {
|
||||
senderId = RecipientId.from(serializedSenderId);
|
||||
}
|
||||
|
||||
String serializedThreadRecipientId = mediaExtras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID);
|
||||
String serializedThreadRecipientId = mediaExtras.getString(VoiceNoteMediaItemFactory.EXTRA_THREAD_RECIPIENT_ID);
|
||||
if (serializedThreadRecipientId != null) {
|
||||
threadRecipientId = RecipientId.from(serializedThreadRecipientId);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -24,9 +27,9 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Factory responsible for building out MediaDescriptionCompat objects for voice notes.
|
||||
* Factory responsible for building out MediaItem objects for voice notes.
|
||||
*/
|
||||
class VoiceNoteMediaDescriptionCompatFactory {
|
||||
class VoiceNoteMediaItemFactory {
|
||||
|
||||
public static final String EXTRA_MESSAGE_POSITION = "voice.note.extra.MESSAGE_POSITION";
|
||||
public static final String EXTRA_THREAD_RECIPIENT_ID = "voice.note.extra.RECIPIENT_ID";
|
||||
@@ -37,13 +40,16 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
|
||||
public static final String EXTRA_MESSAGE_TIMESTAMP = "voice.note.extra.MESSAGE_TIMESTAMP";
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
|
||||
public static final Uri NEXT_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-down.ogg");
|
||||
public static final Uri END_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-up.ogg");
|
||||
|
||||
private VoiceNoteMediaDescriptionCompatFactory() {}
|
||||
private static final String TAG = Log.tag(VoiceNoteMediaItemFactory.class);
|
||||
|
||||
static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull Uri draftUri)
|
||||
private VoiceNoteMediaItemFactory() {}
|
||||
|
||||
static MediaItem buildMediaItem(@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull Uri draftUri)
|
||||
{
|
||||
|
||||
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
@@ -51,28 +57,28 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||
threadRecipient = Recipient.UNKNOWN;
|
||||
}
|
||||
|
||||
return buildMediaDescription(context,
|
||||
threadRecipient,
|
||||
Recipient.self(),
|
||||
Recipient.self(),
|
||||
0,
|
||||
threadId,
|
||||
-1,
|
||||
System.currentTimeMillis(),
|
||||
draftUri);
|
||||
return buildMediaItem(context,
|
||||
threadRecipient,
|
||||
Recipient.self(),
|
||||
Recipient.self(),
|
||||
0,
|
||||
threadId,
|
||||
-1,
|
||||
System.currentTimeMillis(),
|
||||
draftUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build out a MediaDescriptionCompat for a given voice note. Expects to be run
|
||||
* Build out a MediaItem for a given voice note. Expects to be run
|
||||
* on a background thread.
|
||||
*
|
||||
* @param context Context.
|
||||
* @param messageRecord The MessageRecord of the given voice note.
|
||||
* @return A MediaDescriptionCompat with all the details the service expects.
|
||||
* @return A MediaItem with all the details the service expects.
|
||||
*/
|
||||
@WorkerThread
|
||||
@Nullable static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
|
||||
@NonNull MessageRecord messageRecord)
|
||||
@Nullable static MediaItem buildMediaItem(@NonNull Context context,
|
||||
@NonNull MessageRecord messageRecord)
|
||||
{
|
||||
int startingPosition = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
.getMessagePositionInConversation(messageRecord.getThreadId(),
|
||||
@@ -95,26 +101,26 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildMediaDescription(context,
|
||||
threadRecipient,
|
||||
avatarRecipient,
|
||||
sender,
|
||||
startingPosition,
|
||||
messageRecord.getThreadId(),
|
||||
messageRecord.getId(),
|
||||
messageRecord.getDateReceived(),
|
||||
uri);
|
||||
return buildMediaItem(context,
|
||||
threadRecipient,
|
||||
avatarRecipient,
|
||||
sender,
|
||||
startingPosition,
|
||||
messageRecord.getThreadId(),
|
||||
messageRecord.getId(),
|
||||
messageRecord.getDateReceived(),
|
||||
uri);
|
||||
}
|
||||
|
||||
private static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
|
||||
@NonNull Recipient threadRecipient,
|
||||
@NonNull Recipient avatarRecipient,
|
||||
@NonNull Recipient sender,
|
||||
int startingPosition,
|
||||
long threadId,
|
||||
long messageId,
|
||||
long dateReceived,
|
||||
@NonNull Uri audioUri)
|
||||
private static MediaItem buildMediaItem(@NonNull Context context,
|
||||
@NonNull Recipient threadRecipient,
|
||||
@NonNull Recipient avatarRecipient,
|
||||
@NonNull Recipient sender,
|
||||
int startingPosition,
|
||||
long threadId,
|
||||
long messageId,
|
||||
long dateReceived,
|
||||
@NonNull Uri audioUri)
|
||||
{
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(EXTRA_THREAD_RECIPIENT_ID, threadRecipient.getId().serialize());
|
||||
@@ -132,17 +138,28 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||
|
||||
String subtitle = null;
|
||||
if (preference.isDisplayContact()) {
|
||||
subtitle = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__voice_message,
|
||||
subtitle = context.getString(R.string.VoiceNoteMediaItemFactory__voice_message,
|
||||
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(),
|
||||
dateReceived));
|
||||
}
|
||||
|
||||
return new MediaDescriptionCompat.Builder()
|
||||
.setMediaUri(audioUri)
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
.build();
|
||||
return new MediaItem.Builder()
|
||||
.setUri(audioUri)
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
)
|
||||
.setTag(
|
||||
new MediaDescriptionCompat.Builder()
|
||||
.setMediaUri(audioUri)
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static @NonNull String getTitle(@NonNull Context context, @NonNull Recipient sender, @NonNull Recipient threadRecipient, @Nullable NotificationPrivacyPreference notificationPrivacyPreference) {
|
||||
@@ -154,7 +171,7 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||
}
|
||||
|
||||
if (preference.isDisplayContact() && threadRecipient.isGroup()) {
|
||||
return context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__s_to_s,
|
||||
return context.getString(R.string.VoiceNoteMediaItemFactory__s_to_s,
|
||||
sender.getDisplayName(context),
|
||||
threadRecipient.getDisplayName(context));
|
||||
} else if (preference.isDisplayContact()) {
|
||||
@@ -163,4 +180,28 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||
return context.getString(R.string.MessageNotifier_signal_message);
|
||||
}
|
||||
}
|
||||
|
||||
public static MediaItem buildNextVoiceNoteMediaItem(@NonNull MediaItem source) {
|
||||
return cloneMediaItem(source, "next", NEXT_URI);
|
||||
}
|
||||
|
||||
public static MediaItem buildEndVoiceNoteMediaItem(@NonNull MediaItem source) {
|
||||
return cloneMediaItem(source, "end", END_URI);
|
||||
}
|
||||
|
||||
private static MediaItem cloneMediaItem(MediaItem source, String mediaId, Uri uri) {
|
||||
MediaDescriptionCompat description = source.playbackProperties != null ? (MediaDescriptionCompat) source.playbackProperties.tag : null;
|
||||
return source.buildUpon()
|
||||
.setMediaId(mediaId)
|
||||
.setUri(uri)
|
||||
.setTag(
|
||||
description != null ?
|
||||
new MediaDescriptionCompat.Builder()
|
||||
.setMediaUri(uri)
|
||||
.setTitle(description.getTitle())
|
||||
.setSubtitle(description.getSubtitle())
|
||||
.setExtras(description.getExtras())
|
||||
.build() : null)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
||||
public class VoiceNoteNotificationControlDispatcher extends DefaultControlDispatcher {
|
||||
|
||||
private final VoiceNoteQueueDataAdapter dataAdapter;
|
||||
|
||||
public VoiceNoteNotificationControlDispatcher(@NonNull VoiceNoteQueueDataAdapter dataAdapter) {
|
||||
this.dataAdapter = dataAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) {
|
||||
boolean isQueueToneIndex = windowIndex % 2 == 1;
|
||||
boolean isSeekingToStart = positionMs == C.TIME_UNSET;
|
||||
|
||||
if (isQueueToneIndex && isSeekingToStart) {
|
||||
int nextVoiceNoteWindowIndex = player.getCurrentWindowIndex() < windowIndex ? windowIndex + 1 : windowIndex - 1;
|
||||
|
||||
if (dataAdapter.size() <= nextVoiceNoteWindowIndex) {
|
||||
return super.dispatchSeekTo(player, windowIndex, positionMs);
|
||||
} else {
|
||||
return super.dispatchSeekTo(player, nextVoiceNoteWindowIndex, positionMs);
|
||||
}
|
||||
} else {
|
||||
return super.dispatchSeekTo(player, windowIndex, positionMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
|
||||
@@ -16,17 +15,13 @@ import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -40,30 +35,22 @@ class VoiceNoteNotificationManager {
|
||||
|
||||
VoiceNoteNotificationManager(@NonNull Context context,
|
||||
@NonNull MediaSessionCompat.Token token,
|
||||
@NonNull PlayerNotificationManager.NotificationListener listener,
|
||||
@NonNull VoiceNoteQueueDataAdapter dataAdapter)
|
||||
@NonNull PlayerNotificationManager.NotificationListener listener)
|
||||
{
|
||||
this.context = context;
|
||||
|
||||
try {
|
||||
controller = new MediaControllerCompat(context, token);
|
||||
} catch (RemoteException e) {
|
||||
throw new IllegalArgumentException("Could not create a controller with given token");
|
||||
}
|
||||
|
||||
notificationManager = PlayerNotificationManager.createWithNotificationChannel(context,
|
||||
NotificationChannels.VOICE_NOTES,
|
||||
R.string.NotificationChannel_voice_notes,
|
||||
NOW_PLAYING_NOTIFICATION_ID,
|
||||
new DescriptionAdapter());
|
||||
this.context = context;
|
||||
controller = new MediaControllerCompat(context, token);
|
||||
notificationManager = new PlayerNotificationManager.Builder(context, NOW_PLAYING_NOTIFICATION_ID, NotificationChannels.VOICE_NOTES)
|
||||
.setChannelNameResourceId(R.string.NotificationChannel_voice_notes)
|
||||
.setMediaDescriptionAdapter(new DescriptionAdapter())
|
||||
.setNotificationListener(listener)
|
||||
.build();
|
||||
|
||||
notificationManager.setMediaSessionToken(token);
|
||||
notificationManager.setSmallIcon(R.drawable.ic_notification);
|
||||
notificationManager.setRewindIncrementMs(0);
|
||||
notificationManager.setFastForwardIncrementMs(0);
|
||||
notificationManager.setNotificationListener(listener);
|
||||
notificationManager.setColorized(true);
|
||||
notificationManager.setControlDispatcher(new VoiceNoteNotificationControlDispatcher(dataAdapter));
|
||||
notificationManager.setUseFastForwardAction(false);
|
||||
notificationManager.setUseRewindAction(false);
|
||||
notificationManager.setUseStopAction(true);
|
||||
}
|
||||
|
||||
public void hideNotification() {
|
||||
@@ -90,18 +77,20 @@ class VoiceNoteNotificationManager {
|
||||
|
||||
@Override
|
||||
public @Nullable PendingIntent createCurrentContentIntent(Player player) {
|
||||
if (!hasMetadata()) return null;
|
||||
if (!hasMetadata()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String serializedRecipientId = controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID);
|
||||
String serializedRecipientId = controller.getMetadata().getString(VoiceNoteMediaItemFactory.EXTRA_THREAD_RECIPIENT_ID);
|
||||
if (serializedRecipientId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RecipientId recipientId = RecipientId.from(serializedRecipientId);
|
||||
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION);
|
||||
long threadId = controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID);
|
||||
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_POSITION);
|
||||
long threadId = controller.getMetadata().getLong(VoiceNoteMediaItemFactory.EXTRA_THREAD_ID);
|
||||
|
||||
int color = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_COLOR);
|
||||
int color = (int) controller.getMetadata().getLong(VoiceNoteMediaItemFactory.EXTRA_COLOR);
|
||||
|
||||
if (color == 0) {
|
||||
color = ChatColorsPalette.UNKNOWN_CONTACT.asSingleColor();
|
||||
@@ -138,7 +127,7 @@ class VoiceNoteNotificationManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
String serializedRecipientId = controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_AVATAR_RECIPIENT_ID);
|
||||
String serializedRecipientId = controller.getMetadata().getString(VoiceNoteMediaItemFactory.EXTRA_AVATAR_RECIPIENT_ID);
|
||||
if (serializedRecipientId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,50 @@
|
||||
package org.thoughtcrime.securesms.components.voice
|
||||
|
||||
import android.media.AudioManager
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.ControlDispatcher
|
||||
import com.google.android.exoplayer2.PlaybackParameters
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
|
||||
class VoiceNotePlaybackController(private val voiceNotePlaybackParameters: VoiceNotePlaybackParameters) : DefaultPlaybackController() {
|
||||
class VoiceNotePlaybackController(
|
||||
private val player: SimpleExoPlayer,
|
||||
private val voiceNotePlaybackParameters: VoiceNotePlaybackParameters
|
||||
) : MediaSessionConnector.CommandReceiver {
|
||||
|
||||
override fun getCommands(): Array<String> {
|
||||
return arrayOf(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED)
|
||||
}
|
||||
|
||||
override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) {
|
||||
@Suppress("deprecation")
|
||||
override fun onCommand(p: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
||||
if (command == VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED) {
|
||||
val speed = extras?.getFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, 1f) ?: 1f
|
||||
|
||||
player.playbackParameters = PlaybackParameters(speed)
|
||||
voiceNotePlaybackParameters.setSpeed(speed)
|
||||
return true
|
||||
} else if (command == VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM) {
|
||||
val newStreamType: Int = extras?.getInt(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, AudioManager.STREAM_MUSIC) ?: AudioManager.STREAM_MUSIC
|
||||
|
||||
val currentStreamType = Util.getStreamTypeForAudioUsage(player.audioAttributes.usage)
|
||||
if (newStreamType != currentStreamType) {
|
||||
val attributes = when (newStreamType) {
|
||||
AudioManager.STREAM_MUSIC -> AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build()
|
||||
AudioManager.STREAM_VOICE_CALL -> AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_SPEECH).setUsage(C.USAGE_VOICE_COMMUNICATION).build()
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
|
||||
player.playWhenReady = false
|
||||
player.setAudioAttributes(attributes, false)
|
||||
|
||||
if (newStreamType == AudioManager.STREAM_VOICE_CALL) {
|
||||
player.playWhenReady = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -14,11 +13,12 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.exoplayer2.ControlDispatcher;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -26,12 +26,9 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -49,30 +46,19 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
private static final long LIMIT = 5;
|
||||
|
||||
public static final Uri NEXT_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-down.ogg");
|
||||
public static final Uri END_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-up.ogg");
|
||||
|
||||
private final Context context;
|
||||
private final SimpleExoPlayer player;
|
||||
private final VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||
private final AttachmentMediaSourceFactory mediaSourceFactory;
|
||||
private final ConcatenatingMediaSource dataSource;
|
||||
private final Context context;
|
||||
private final Player player;
|
||||
private final VoiceNotePlaybackParameters voiceNotePlaybackParameters;
|
||||
|
||||
private boolean canLoadMore;
|
||||
private Uri latestUri = Uri.EMPTY;
|
||||
|
||||
VoiceNotePlaybackPreparer(@NonNull Context context,
|
||||
@NonNull SimpleExoPlayer player,
|
||||
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
|
||||
@NonNull AttachmentMediaSourceFactory mediaSourceFactory,
|
||||
@NonNull Player player,
|
||||
@NonNull VoiceNotePlaybackParameters voiceNotePlaybackParameters)
|
||||
{
|
||||
this.context = context;
|
||||
this.player = player;
|
||||
this.queueDataAdapter = queueDataAdapter;
|
||||
this.mediaSourceFactory = mediaSourceFactory;
|
||||
this.dataSource = new ConcatenatingMediaSource();
|
||||
this.context = context;
|
||||
this.player = player;
|
||||
this.voiceNotePlaybackParameters = voiceNotePlaybackParameters;
|
||||
}
|
||||
|
||||
@@ -82,23 +68,26 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepare() {
|
||||
public void onPrepare(boolean playWhenReady) {
|
||||
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepare");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
||||
public void onPrepareFromMediaId(@NonNull String mediaId, boolean playWhenReady, @Nullable Bundle extras) {
|
||||
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromMediaId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromSearch(String query, Bundle extras) {
|
||||
public void onPrepareFromSearch(@NonNull String query, boolean playWhenReady, @Nullable Bundle extras) {
|
||||
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromSearch");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromUri(final Uri uri, Bundle extras) {
|
||||
public void onPrepareFromUri(@NonNull Uri uri, boolean playWhenReady, @Nullable Bundle extras) {
|
||||
Log.d(TAG, "onPrepareFromUri: " + uri);
|
||||
if (extras == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
|
||||
long threadId = extras.getLong(VoiceNoteMediaController.EXTRA_THREAD_ID);
|
||||
@@ -112,26 +101,25 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||
() -> {
|
||||
if (singlePlayback) {
|
||||
if (messageId != -1) {
|
||||
return loadMediaDescriptionForSinglePlayback(messageId);
|
||||
return loadMediaItemsForSinglePlayback(messageId);
|
||||
} else {
|
||||
return loadMediaDescriptionForDraftPlayback(threadId, uri);
|
||||
return loadMediaItemsForDraftPlayback(threadId, uri);
|
||||
}
|
||||
} else {
|
||||
return loadMediaDescriptionsForConsecutivePlayback(messageId);
|
||||
return loadMediaItemsForConsecutivePlayback(messageId);
|
||||
}
|
||||
},
|
||||
descriptions -> {
|
||||
queueDataAdapter.clear();
|
||||
dataSource.clear();
|
||||
mediaItems -> {
|
||||
player.clearMediaItems();
|
||||
|
||||
if (Util.hasItems(descriptions) && Objects.equals(latestUri, uri)) {
|
||||
applyDescriptionsToQueue(descriptions);
|
||||
if (Util.hasItems(mediaItems) && Objects.equals(latestUri, uri)) {
|
||||
applyDescriptionsToQueue(mediaItems);
|
||||
|
||||
int window = Math.max(0, queueDataAdapter.indexOf(uri));
|
||||
int window = Math.max(0, indexOfPlayerMediaItemByUri(uri));
|
||||
|
||||
player.addListener(new Player.EventListener() {
|
||||
player.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
|
||||
public void onTimelineChanged(@NonNull Timeline timeline, int reason) {
|
||||
if (timeline.getWindowCount() >= window) {
|
||||
player.setPlayWhenReady(false);
|
||||
player.setPlaybackParameters(voiceNotePlaybackParameters.getParameters());
|
||||
@@ -142,102 +130,91 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||
}
|
||||
});
|
||||
|
||||
player.prepare(dataSource);
|
||||
player.prepare();
|
||||
canLoadMore = !singlePlayback;
|
||||
} else if (Objects.equals(latestUri, uri)) {
|
||||
Log.w(TAG, "Requested playback but no voice notes could be found.");
|
||||
ThreadUtil.postToMain(() -> Toast.makeText(context, R.string.VoiceNotePlaybackPreparer__failed_to_play_voice_message, Toast.LENGTH_SHORT)
|
||||
.show());
|
||||
ThreadUtil.postToMain(() -> {
|
||||
Toast.makeText(context, R.string.VoiceNotePlaybackPreparer__failed_to_play_voice_message, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCommands() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void applyDescriptionsToQueue(@NonNull List<MediaDescriptionCompat> descriptions) {
|
||||
for (MediaDescriptionCompat description : descriptions) {
|
||||
int holderIndex = queueDataAdapter.indexOf(description.getMediaUri());
|
||||
MediaDescriptionCompat next = createNextClone(description);
|
||||
int currentIndex = player.getCurrentWindowIndex();
|
||||
private void applyDescriptionsToQueue(@NonNull List<MediaItem> mediaItems) {
|
||||
for (MediaItem mediaItem : mediaItems) {
|
||||
MediaItem.PlaybackProperties playbackProperties = mediaItem.playbackProperties;
|
||||
if (playbackProperties == null) {
|
||||
continue;
|
||||
}
|
||||
int holderIndex = indexOfPlayerMediaItemByUri(playbackProperties.uri);
|
||||
MediaItem next = VoiceNoteMediaItemFactory.buildNextVoiceNoteMediaItem(mediaItem);
|
||||
int currentIndex = player.getCurrentWindowIndex();
|
||||
|
||||
if (holderIndex != -1) {
|
||||
queueDataAdapter.remove(holderIndex);
|
||||
|
||||
if (!queueDataAdapter.isEmpty()) {
|
||||
queueDataAdapter.remove(holderIndex);
|
||||
}
|
||||
|
||||
queueDataAdapter.add(holderIndex, createNextClone(description));
|
||||
queueDataAdapter.add(holderIndex, description);
|
||||
|
||||
if (currentIndex != holderIndex) {
|
||||
dataSource.removeMediaSource(holderIndex);
|
||||
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
|
||||
player.removeMediaItem(holderIndex);
|
||||
player.addMediaItem(holderIndex, mediaItem);
|
||||
}
|
||||
|
||||
if (currentIndex != holderIndex + 1) {
|
||||
if (dataSource.getSize() > 1) {
|
||||
dataSource.removeMediaSource(holderIndex + 1);
|
||||
if (player.getMediaItemCount() > 1) {
|
||||
player.removeMediaItem(holderIndex + 1);
|
||||
}
|
||||
|
||||
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
|
||||
player.addMediaItem(holderIndex + 1, next);
|
||||
}
|
||||
} else {
|
||||
int insertLocation = queueDataAdapter.indexAfter(description);
|
||||
int insertLocation = indexAfter(mediaItem);
|
||||
|
||||
queueDataAdapter.add(insertLocation, next);
|
||||
queueDataAdapter.add(insertLocation, description);
|
||||
|
||||
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
|
||||
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
|
||||
player.addMediaItem(insertLocation, next);
|
||||
player.addMediaItem(insertLocation, mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
int lastIndex = queueDataAdapter.size() - 1;
|
||||
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
|
||||
int itemsCount = player.getMediaItemCount();
|
||||
if (itemsCount > 0) {
|
||||
int lastIndex = itemsCount - 1;
|
||||
MediaItem last = player.getMediaItemAt(lastIndex);
|
||||
|
||||
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
|
||||
queueDataAdapter.remove(lastIndex);
|
||||
dataSource.removeMediaSource(lastIndex);
|
||||
if (last.playbackProperties != null &&
|
||||
Objects.equals(last.playbackProperties.uri, VoiceNoteMediaItemFactory.NEXT_URI))
|
||||
{
|
||||
player.removeMediaItem(lastIndex);
|
||||
|
||||
if (queueDataAdapter.size() > 1) {
|
||||
MediaDescriptionCompat end = createEndClone(last);
|
||||
if (player.getMediaItemCount() > 1) {
|
||||
MediaItem end = VoiceNoteMediaItemFactory.buildEndVoiceNoteMediaItem(last);
|
||||
|
||||
queueDataAdapter.add(lastIndex, end);
|
||||
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
|
||||
player.addMediaItem(lastIndex, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (queueDataAdapter.size() != dataSource.getSize()) {
|
||||
throw new IllegalStateException("QueueDataAdapter and DataSource size inconsistency.");
|
||||
private int indexOfPlayerMediaItemByUri(@NonNull Uri uri) {
|
||||
for (int i = 0; i < player.getMediaItemCount(); i++) {
|
||||
MediaItem.PlaybackProperties playbackProperties = player.getMediaItemAt(i).playbackProperties;
|
||||
if (playbackProperties != null && playbackProperties.uri.equals(uri)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private @NonNull MediaDescriptionCompat createEndClone(@NonNull MediaDescriptionCompat source) {
|
||||
return buildUpon(source).setMediaId("end").setMediaUri(END_URI).build();
|
||||
}
|
||||
private int indexAfter(@NonNull MediaItem target) {
|
||||
int size = player.getMediaItemCount();
|
||||
long targetMessageId = target.mediaMetadata.extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
|
||||
for (int i = 0; i < size; i++) {
|
||||
MediaMetadata mediaMetadata = player.getMediaItemAt(i).mediaMetadata;
|
||||
long messageId = mediaMetadata.extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
|
||||
|
||||
private @NonNull MediaDescriptionCompat createNextClone(@NonNull MediaDescriptionCompat source) {
|
||||
return buildUpon(source).setMediaId("next").setMediaUri(NEXT_URI).build();
|
||||
}
|
||||
|
||||
private @NonNull MediaDescriptionCompat.Builder buildUpon(@NonNull MediaDescriptionCompat source) {
|
||||
return new MediaDescriptionCompat.Builder()
|
||||
.setSubtitle(source.getSubtitle())
|
||||
.setDescription(source.getDescription())
|
||||
.setTitle(source.getTitle())
|
||||
.setIconUri(source.getIconUri())
|
||||
.setIconBitmap(source.getIconBitmap())
|
||||
.setMediaId(source.getMediaId())
|
||||
.setExtras(source.getExtras());
|
||||
if (messageId > targetMessageId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
public void loadMoreVoiceNotes() {
|
||||
@@ -245,36 +222,37 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||
return;
|
||||
}
|
||||
|
||||
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
|
||||
if (Objects.equals(mediaDescriptionCompat, VoiceNoteQueueDataAdapter.EMPTY)) {
|
||||
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
||||
if (currentMediaItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long messageId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
long messageId = currentMediaItem.mediaMetadata.extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
|
||||
|
||||
SimpleTask.run(EXECUTOR,
|
||||
() -> loadMediaDescriptionsForConsecutivePlayback(messageId),
|
||||
descriptions -> {
|
||||
if (Util.hasItems(descriptions) && canLoadMore) {
|
||||
applyDescriptionsToQueue(descriptions);
|
||||
() -> loadMediaItemsForConsecutivePlayback(messageId),
|
||||
mediaItems -> {
|
||||
if (Util.hasItems(mediaItems) && canLoadMore) {
|
||||
applyDescriptionsToQueue(mediaItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionForSinglePlayback(long messageId) {
|
||||
private @NonNull List<MediaItem> loadMediaItemsForSinglePlayback(long messageId) {
|
||||
try {
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(context)
|
||||
.getMessageRecord(messageId);
|
||||
|
||||
if (!MessageRecordUtil.hasAudio(messageRecord)) {
|
||||
Log.w(TAG, "Message does not contain audio.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
MediaDescriptionCompat mediaDescriptionCompat = VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context ,messageRecord);
|
||||
if (mediaDescriptionCompat == null) {
|
||||
MediaItem mediaItem = VoiceNoteMediaItemFactory.buildMediaItem(context, messageRecord);
|
||||
if (mediaItem == null) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
return Collections.singletonList(mediaDescriptionCompat);
|
||||
return Collections.singletonList(mediaItem);
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Could not find message.", e);
|
||||
@@ -282,17 +260,20 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionForDraftPlayback(long threadId, @NonNull Uri draftUri) {
|
||||
return Collections.singletonList(VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, threadId, draftUri));
|
||||
private @NonNull List<MediaItem> loadMediaItemsForDraftPlayback(long threadId, @NonNull Uri draftUri) {
|
||||
return Collections
|
||||
.singletonList(VoiceNoteMediaItemFactory.buildMediaItem(context, threadId, draftUri));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionsForConsecutivePlayback(long messageId) {
|
||||
private @NonNull List<MediaItem> loadMediaItemsForConsecutivePlayback(long messageId) {
|
||||
try {
|
||||
List<MessageRecord> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context).getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
|
||||
List<MessageRecord> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
.getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
|
||||
|
||||
return buildFilteredMessageRecordList(recordsAfter).stream()
|
||||
.map(record -> VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, record))
|
||||
.map(record -> VoiceNoteMediaItemFactory
|
||||
.buildMediaItem(context, record))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
} catch (NoSuchMessageException e) {
|
||||
@@ -306,4 +287,15 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||
.takeWhile(MessageRecordUtil::hasAudio)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public boolean onCommand(@NonNull Player player,
|
||||
@NonNull ControlDispatcher controlDispatcher,
|
||||
@NonNull String command,
|
||||
@Nullable Bundle extras,
|
||||
@Nullable ResultReceiver cb)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
@@ -21,22 +20,15 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.media.MediaBrowserServiceCompat;
|
||||
|
||||
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.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
@@ -45,7 +37,6 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -56,6 +47,7 @@ import java.util.List;
|
||||
public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
|
||||
public static final String ACTION_NEXT_PLAYBACK_SPEED = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService.action.next_playback_speed";
|
||||
public static final String ACTION_SET_AUDIO_STREAM = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService.action.set_audio_stream";
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNotePlaybackService.class);
|
||||
private static final String EMPTY_ROOT_ID = "empty-root-id";
|
||||
@@ -69,58 +61,38 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
|
||||
private MediaSessionCompat mediaSession;
|
||||
private MediaSessionConnector mediaSessionConnector;
|
||||
private PlaybackStateCompat.Builder stateBuilder;
|
||||
private SimpleExoPlayer player;
|
||||
private VoiceNotePlayer player;
|
||||
private BecomingNoisyReceiver becomingNoisyReceiver;
|
||||
private KeyClearedReceiver keyClearedReceiver;
|
||||
private VoiceNoteNotificationManager voiceNoteNotificationManager;
|
||||
private VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||
private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer;
|
||||
private VoiceNoteProximityManager voiceNoteProximityManager;
|
||||
private boolean isForegroundService;
|
||||
private VoiceNotePlaybackParameters voiceNotePlaybackParameters;
|
||||
|
||||
private final LoadControl loadControl = new DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE)
|
||||
.createDefaultLoadControl();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
mediaSession = new MediaSessionCompat(this, TAG);
|
||||
voiceNotePlaybackParameters = new VoiceNotePlaybackParameters(mediaSession);
|
||||
stateBuilder = new PlaybackStateCompat.Builder()
|
||||
.setActions(SUPPORTED_ACTIONS)
|
||||
.addCustomAction(ACTION_NEXT_PLAYBACK_SPEED, "speed", R.drawable.ic_toggle_24);
|
||||
mediaSessionConnector = new MediaSessionConnector(mediaSession, new VoiceNotePlaybackController(voiceNotePlaybackParameters));
|
||||
mediaSessionConnector = new MediaSessionConnector(mediaSession);
|
||||
becomingNoisyReceiver = new BecomingNoisyReceiver(this, mediaSession.getSessionToken());
|
||||
keyClearedReceiver = new KeyClearedReceiver(this, mediaSession.getSessionToken());
|
||||
player = ExoPlayerFactory.newSimpleInstance(this, new DefaultRenderersFactory(this), new DefaultTrackSelector(), loadControl);
|
||||
queueDataAdapter = new VoiceNoteQueueDataAdapter();
|
||||
player = new VoiceNotePlayer(this);
|
||||
voiceNoteNotificationManager = new VoiceNoteNotificationManager(this,
|
||||
mediaSession.getSessionToken(),
|
||||
new VoiceNoteNotificationManagerListener(),
|
||||
queueDataAdapter);
|
||||
|
||||
AttachmentMediaSourceFactory mediaSourceFactory = new AttachmentMediaSourceFactory(this);
|
||||
|
||||
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory, voiceNotePlaybackParameters);
|
||||
voiceNoteProximityManager = new VoiceNoteProximityManager(this, player, queueDataAdapter);
|
||||
|
||||
mediaSession.setPlaybackState(stateBuilder.build());
|
||||
new VoiceNoteNotificationManagerListener());
|
||||
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, voiceNotePlaybackParameters);
|
||||
|
||||
player.addListener(new VoiceNotePlayerEventListener());
|
||||
player.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(C.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build(), true);
|
||||
|
||||
mediaSessionConnector.setPlayer(player, voiceNotePlaybackPreparer);
|
||||
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession, queueDataAdapter));
|
||||
mediaSessionConnector.setPlayer(player);
|
||||
mediaSessionConnector.setEnabledPlaybackActions(SUPPORTED_ACTIONS);
|
||||
mediaSessionConnector.setPlaybackPreparer(voiceNotePlaybackPreparer);
|
||||
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession));
|
||||
|
||||
VoiceNotePlaybackController voiceNotePlaybackController = new VoiceNotePlaybackController(player.getInternalPlayer(), voiceNotePlaybackParameters);
|
||||
mediaSessionConnector.registerCustomCommandReceiver(voiceNotePlaybackController);
|
||||
|
||||
setSessionToken(mediaSession.getSessionToken());
|
||||
|
||||
@@ -132,7 +104,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
|
||||
player.stop(true);
|
||||
player.stop();
|
||||
player.clearMediaItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,10 +132,19 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
result.sendResult(Collections.emptyList());
|
||||
}
|
||||
|
||||
private class VoiceNotePlayerEventListener implements Player.EventListener {
|
||||
private class VoiceNotePlayerEventListener implements Player.Listener {
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||
onPlaybackStateChanged(playWhenReady, player.getPlaybackState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
onPlaybackStateChanged(player.getPlayWhenReady(), playbackState);
|
||||
}
|
||||
|
||||
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
|
||||
switch (playbackState) {
|
||||
case Player.STATE_BUFFERING:
|
||||
case Player.STATE_READY:
|
||||
@@ -170,46 +152,48 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
|
||||
if (!playWhenReady) {
|
||||
stopForeground(false);
|
||||
isForegroundService = false;
|
||||
becomingNoisyReceiver.unregister();
|
||||
voiceNoteProximityManager.onPlayerEnded();
|
||||
} else {
|
||||
sendViewedReceiptForCurrentWindowIndex();
|
||||
becomingNoisyReceiver.register();
|
||||
voiceNoteProximityManager.onPlayerReady();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
voiceNoteProximityManager.onPlayerEnded();
|
||||
becomingNoisyReceiver.unregister();
|
||||
voiceNoteNotificationManager.hideNotification();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(int reason) {
|
||||
int currentWindowIndex = player.getCurrentWindowIndex();
|
||||
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
|
||||
int currentWindowIndex = newPosition.windowIndex;
|
||||
if (currentWindowIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
|
||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
sendViewedReceiptForCurrentWindowIndex();
|
||||
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
|
||||
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + mediaDescriptionCompat.getMediaUri());
|
||||
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
||||
if (currentMediaItem != null && currentMediaItem.playbackProperties != null) {
|
||||
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + currentMediaItem.playbackProperties.uri);
|
||||
}
|
||||
|
||||
PlaybackParameters playbackParameters = getPlaybackParametersForWindowPosition(currentWindowIndex);
|
||||
|
||||
final float speed = playbackParameters != null ? playbackParameters.speed : 1f;
|
||||
if (speed != player.getPlaybackParameters().speed) {
|
||||
player.setPlayWhenReady(false);
|
||||
player.setPlaybackParameters(playbackParameters);
|
||||
if (playbackParameters != null) {
|
||||
player.setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
player.seekTo(currentWindowIndex, 1);
|
||||
player.setPlayWhenReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
|
||||
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();
|
||||
currentWindowIndex + LOAD_MORE_THRESHOLD >= player.getMediaItemCount();
|
||||
|
||||
if (isWithinThreshold && currentWindowIndex % 2 == 0) {
|
||||
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
|
||||
@@ -217,9 +201,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
public void onPlayerError(@NonNull PlaybackException error) {
|
||||
Log.w(TAG, "ExoPlayer error occurred:", error);
|
||||
voiceNoteProximityManager.onPlayerError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,16 +224,23 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
player.getCurrentWindowIndex() != C.INDEX_UNSET)
|
||||
{
|
||||
|
||||
final MediaDescriptionCompat descriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
|
||||
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
||||
if (currentMediaItem == null || currentMediaItem.playbackProperties == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!descriptionCompat.getMediaUri().getScheme().equals("content")) {
|
||||
Uri mediaUri = currentMediaItem.playbackProperties.uri;
|
||||
if (!mediaUri.getScheme().equals("content")) {
|
||||
return;
|
||||
}
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
Bundle extras = descriptionCompat.getExtras();
|
||||
long messageId = extras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID));
|
||||
Bundle extras = currentMediaItem.mediaMetadata.extras;
|
||||
if (extras == null) {
|
||||
return;
|
||||
}
|
||||
long messageId = extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
|
||||
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaItemFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID));
|
||||
MessageDatabase messageDatabase = DatabaseFactory.getMmsDatabase(this);
|
||||
|
||||
MessageDatabase.MarkedMessageInfo markedMessageInfo = messageDatabase.setIncomingMessageViewed(messageId);
|
||||
@@ -269,8 +259,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
|
||||
|
||||
@Override
|
||||
public void onNotificationStarted(int notificationId, Notification notification) {
|
||||
if (!isForegroundService) {
|
||||
public void onNotificationPosted(int notificationId, Notification notification, boolean ongoing) {
|
||||
if (ongoing && !isForegroundService) {
|
||||
ContextCompat.startForegroundService(getApplicationContext(), new Intent(getApplicationContext(), VoiceNotePlaybackService.class));
|
||||
startForeground(notificationId, notification);
|
||||
isForegroundService = true;
|
||||
@@ -278,7 +268,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationCancelled(int notificationId) {
|
||||
public void onNotificationCancelled(int notificationId, boolean dismissedByUser) {
|
||||
stopForeground(true);
|
||||
isForegroundService = false;
|
||||
stopSelf();
|
||||
@@ -297,12 +287,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
private boolean registered;
|
||||
|
||||
private KeyClearedReceiver(@NonNull Context context, @NonNull MediaSessionCompat.Token token) {
|
||||
this.context = context;
|
||||
try {
|
||||
this.controller = new MediaControllerCompat(context, token);
|
||||
} catch (RemoteException e) {
|
||||
throw new IllegalArgumentException("Failed to create controller from token", e);
|
||||
}
|
||||
this.context = context;
|
||||
this.controller = new MediaControllerCompat(context, token);
|
||||
}
|
||||
|
||||
void register() {
|
||||
@@ -337,12 +323,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
private boolean registered;
|
||||
|
||||
private BecomingNoisyReceiver(Context context, MediaSessionCompat.Token token) {
|
||||
this.context = context;
|
||||
try {
|
||||
this.controller = new MediaControllerCompat(context, token);
|
||||
} catch (RemoteException e) {
|
||||
throw new IllegalArgumentException("Failed to create controller from token", e);
|
||||
}
|
||||
this.context = context;
|
||||
this.controller = new MediaControllerCompat(context, token);
|
||||
}
|
||||
|
||||
void register() {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.components.voice
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.DefaultLoadControl
|
||||
import com.google.android.exoplayer2.ForwardingPlayer
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory
|
||||
|
||||
class VoiceNotePlayer @JvmOverloads constructor(
|
||||
context: Context,
|
||||
val internalPlayer: SimpleExoPlayer = SimpleExoPlayer.Builder(context)
|
||||
.setMediaSourceFactory(AttachmentMediaSourceFactory(context))
|
||||
.setLoadControl(
|
||||
DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)
|
||||
.build()
|
||||
).build()
|
||||
) : ForwardingPlayer(internalPlayer) {
|
||||
|
||||
override fun seekTo(windowIndex: Int, positionMs: Long) {
|
||||
super.seekTo(windowIndex, positionMs)
|
||||
|
||||
val isQueueToneIndex = windowIndex % 2 == 1
|
||||
val isSeekingToStart = positionMs == C.TIME_UNSET
|
||||
|
||||
return if (isQueueToneIndex && isSeekingToStart) {
|
||||
val nextVoiceNoteWindowIndex = if (currentWindowIndex < windowIndex) windowIndex + 1 else windowIndex - 1
|
||||
if (mediaItemCount <= nextVoiceNoteWindowIndex) {
|
||||
super.seekTo(windowIndex, positionMs)
|
||||
} else {
|
||||
super.seekTo(nextVoiceNoteWindowIndex, positionMs)
|
||||
}
|
||||
} else {
|
||||
super.seekTo(windowIndex, positionMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,7 @@ class VoiceNotePlayerView @JvmOverloads constructor(
|
||||
background.colorFilter = SimpleColorFilter(ContextCompat.getColor(context, R.color.voice_note_player_view_background))
|
||||
}
|
||||
|
||||
contentDescription = context.getString(R.string.VoiceNotePlayerView__navigate_to_voice_message)
|
||||
setOnClickListener {
|
||||
lastState?.let {
|
||||
listener?.onNavigateToMessage(it.threadId, it.threadRecipientId, it.senderId, it.messageTimestamp, it.messagePositionInThread)
|
||||
@@ -171,6 +172,12 @@ class VoiceNotePlayerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
lottieDirection = direction
|
||||
playPauseToggleView.contentDescription = if (direction == TO_PLAY) {
|
||||
context.getString(R.string.VoiceNotePlayerView__play_voice_message)
|
||||
} else {
|
||||
context.getString(R.string.VoiceNotePlayerView__pause_voice_message)
|
||||
}
|
||||
|
||||
playPauseToggleView.pauseAnimation()
|
||||
playPauseToggleView.speed = (direction * 2).toFloat()
|
||||
playPauseToggleView.resumeAnimation()
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
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.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class VoiceNoteProximityManager implements SensorEventListener {
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteProximityManager.class);
|
||||
|
||||
private static final float PROXIMITY_THRESHOLD = 5f;
|
||||
|
||||
private final SimpleExoPlayer player;
|
||||
private final AudioManager audioManager;
|
||||
private final SensorManager sensorManager;
|
||||
private final Sensor proximitySensor;
|
||||
private final PowerManager.WakeLock wakeLock;
|
||||
private final VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||
|
||||
private long startTime;
|
||||
|
||||
VoiceNoteProximityManager(@NonNull Context context,
|
||||
@NonNull SimpleExoPlayer player,
|
||||
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter)
|
||||
{
|
||||
this.player = player;
|
||||
this.audioManager = ServiceUtil.getAudioManager(context);
|
||||
this.sensorManager = ServiceUtil.getSensorManager(context);
|
||||
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
|
||||
this.queueDataAdapter = queueDataAdapter;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
|
||||
} else {
|
||||
this.wakeLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
void onPlayerReady() {
|
||||
startTime = System.currentTimeMillis();
|
||||
sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
|
||||
}
|
||||
|
||||
void onPlayerEnded() {
|
||||
sensorManager.unregisterListener(this);
|
||||
|
||||
if (wakeLock != null && wakeLock.isHeld() && Build.VERSION.SDK_INT >= 21) {
|
||||
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
||||
}
|
||||
}
|
||||
|
||||
void onPlayerError() {
|
||||
onPlayerEnded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY || player.getPlaybackState() != Player.STATE_READY) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int desiredStreamType;
|
||||
if (event.values[0] < PROXIMITY_THRESHOLD && event.values[0] != proximitySensor.getMaximumRange()) {
|
||||
desiredStreamType = AudioManager.STREAM_VOICE_CALL;
|
||||
} else {
|
||||
desiredStreamType = AudioManager.STREAM_MUSIC;
|
||||
}
|
||||
|
||||
final int currentStreamType = Util.getStreamTypeForAudioUsage(player.getAudioAttributes().usage);
|
||||
|
||||
final long threadId;
|
||||
final int windowIndex = player.getCurrentWindowIndex();
|
||||
|
||||
if (queueDataAdapter.isEmpty() || windowIndex == C.INDEX_UNSET) {
|
||||
threadId = -1;
|
||||
} else {
|
||||
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(windowIndex);
|
||||
|
||||
if (mediaDescriptionCompat.getExtras() == null) {
|
||||
threadId = -1;
|
||||
} else {
|
||||
threadId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID, -1);
|
||||
}
|
||||
}
|
||||
|
||||
if (desiredStreamType == AudioManager.STREAM_VOICE_CALL &&
|
||||
desiredStreamType != currentStreamType &&
|
||||
!audioManager.isWiredHeadsetOn() &&
|
||||
threadId != -1 &&
|
||||
ApplicationDependencies.getMessageNotifier().getVisibleThread() == threadId)
|
||||
{
|
||||
if (wakeLock != null && !wakeLock.isHeld()) {
|
||||
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30));
|
||||
}
|
||||
|
||||
player.setPlayWhenReady(false);
|
||||
player.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(C.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(C.USAGE_VOICE_COMMUNICATION)
|
||||
.build());
|
||||
player.setPlayWhenReady(true);
|
||||
|
||||
startTime = System.currentTimeMillis();
|
||||
} else if (desiredStreamType == AudioManager.STREAM_MUSIC &&
|
||||
desiredStreamType != currentStreamType &&
|
||||
System.currentTimeMillis() - startTime > 500)
|
||||
{
|
||||
if (wakeLock != null) {
|
||||
if (wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
|
||||
player.setPlayWhenReady(false);
|
||||
player.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(C.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package org.thoughtcrime.securesms.components.voice
|
||||
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.media.session.MediaControllerCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private val TAG = Log.tag(VoiceNoteProximityWakeLockManager::class.java)
|
||||
private const val PROXIMITY_THRESHOLD = 5f
|
||||
|
||||
/**
|
||||
* Manages the WakeLock while a VoiceNote is playing back in the target activity.
|
||||
*/
|
||||
class VoiceNoteProximityWakeLockManager(
|
||||
private val activity: AppCompatActivity,
|
||||
private val mediaController: MediaControllerCompat
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
private val wakeLock: PowerManager.WakeLock? = if (Build.VERSION.SDK_INT >= 21) {
|
||||
ServiceUtil.getPowerManager(activity).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private val sensorManager: SensorManager = ServiceUtil.getSensorManager(activity)
|
||||
private val proximitySensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
|
||||
|
||||
private val mediaControllerCallback = MediaControllerCallback()
|
||||
private val hardwareSensorEventListener = HardwareSensorEventListener()
|
||||
|
||||
private var startTime: Long = -1
|
||||
|
||||
init {
|
||||
if (proximitySensor != null) {
|
||||
activity.lifecycle.addObserver(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
if (proximitySensor != null) {
|
||||
mediaController.registerCallback(mediaControllerCallback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
if (proximitySensor != null) {
|
||||
unregisterCallbacksAndRelease()
|
||||
}
|
||||
}
|
||||
|
||||
fun unregisterCallbacksAndRelease() {
|
||||
mediaController.unregisterCallback(mediaControllerCallback)
|
||||
cleanUpWakeLock()
|
||||
}
|
||||
|
||||
private fun isActivityResumed() = activity.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
|
||||
|
||||
private fun isPlayerActive() = mediaController.playbackState.state == PlaybackStateCompat.STATE_BUFFERING ||
|
||||
mediaController.playbackState.state == PlaybackStateCompat.STATE_PLAYING
|
||||
|
||||
private fun cleanUpWakeLock() {
|
||||
startTime = -1L
|
||||
sensorManager.unregisterListener(hardwareSensorEventListener)
|
||||
|
||||
if (wakeLock?.isHeld == true) {
|
||||
wakeLock.release()
|
||||
}
|
||||
|
||||
sendNewStreamTypeToPlayer(AudioManager.STREAM_MUSIC)
|
||||
}
|
||||
|
||||
private fun sendNewStreamTypeToPlayer(newStreamType: Int) {
|
||||
val params = Bundle()
|
||||
params.putInt(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, newStreamType)
|
||||
mediaController.sendCommand(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, params, null)
|
||||
}
|
||||
|
||||
inner class MediaControllerCallback : MediaControllerCompat.Callback() {
|
||||
override fun onPlaybackStateChanged(state: PlaybackStateCompat) {
|
||||
if (!isActivityResumed()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isPlayerActive()) {
|
||||
if (startTime == -1L) {
|
||||
startTime = System.currentTimeMillis()
|
||||
sensorManager.registerListener(hardwareSensorEventListener, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL)
|
||||
}
|
||||
} else {
|
||||
cleanUpWakeLock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class HardwareSensorEventListener : SensorEventListener {
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
if (startTime == -1L ||
|
||||
System.currentTimeMillis() - startTime <= 500 ||
|
||||
!isActivityResumed() ||
|
||||
!isPlayerActive() ||
|
||||
event.sensor.type != Sensor.TYPE_PROXIMITY
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
val newStreamType = if (event.values[0] < PROXIMITY_THRESHOLD && event.values[0] != proximitySensor?.maximumRange) {
|
||||
AudioManager.STREAM_VOICE_CALL
|
||||
} else {
|
||||
AudioManager.STREAM_MUSIC
|
||||
}
|
||||
|
||||
sendNewStreamTypeToPlayer(newStreamType)
|
||||
|
||||
if (newStreamType == AudioManager.STREAM_VOICE_CALL) {
|
||||
if (wakeLock?.isHeld == false) {
|
||||
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
||||
}
|
||||
|
||||
startTime = System.currentTimeMillis()
|
||||
} else {
|
||||
if (wakeLock?.isHeld == true) {
|
||||
wakeLock.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* DataAdapter which maintains the current queue of MediaDescriptionCompat objects.
|
||||
*/
|
||||
@MainThread
|
||||
final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAdapter {
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteQueueDataAdapter.class);
|
||||
|
||||
public static final MediaDescriptionCompat EMPTY = new MediaDescriptionCompat.Builder().build();
|
||||
|
||||
private final List<MediaDescriptionCompat> descriptions = new LinkedList<>();
|
||||
|
||||
@Override
|
||||
public MediaDescriptionCompat getMediaDescription(int position) {
|
||||
if (descriptions.size() <= position) {
|
||||
Log.i(TAG, "getMediaDescription: Returning EMPTY MediaDescriptionCompat for index " + position);
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return descriptions.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int position, MediaDescriptionCompat description) {
|
||||
descriptions.add(position, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(int position) {
|
||||
descriptions.remove(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(int from, int to) {
|
||||
MediaDescriptionCompat description = descriptions.remove(from);
|
||||
descriptions.add(to, description);
|
||||
}
|
||||
|
||||
int size() {
|
||||
return descriptions.size();
|
||||
}
|
||||
|
||||
int indexOf(@NonNull Uri uri) {
|
||||
for (int i = 0; i < descriptions.size(); i++) {
|
||||
if (Objects.equals(uri, descriptions.get(i).getMediaUri())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int indexAfter(@NonNull MediaDescriptionCompat target) {
|
||||
if (isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long targetMessageId = target.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
for (int i = 0; i < descriptions.size(); i++) {
|
||||
long descriptionMessageId = descriptions.get(i).getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
|
||||
if (descriptionMessageId > targetMessageId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return descriptions.size();
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return descriptions.isEmpty();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
descriptions.clear();
|
||||
}
|
||||
}
|
||||
@@ -5,24 +5,33 @@ import android.support.v4.media.session.MediaSessionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator;
|
||||
|
||||
/**
|
||||
* Navigator to help support seek forward and back.
|
||||
*/
|
||||
final class VoiceNoteQueueNavigator extends TimelineQueueNavigator {
|
||||
private static final MediaDescriptionCompat EMPTY = new MediaDescriptionCompat.Builder().build();
|
||||
|
||||
private final TimelineQueueEditor.QueueDataAdapter queueDataAdapter;
|
||||
|
||||
public VoiceNoteQueueNavigator(@NonNull MediaSessionCompat mediaSession, @NonNull TimelineQueueEditor.QueueDataAdapter queueDataAdapter) {
|
||||
public VoiceNoteQueueNavigator(@NonNull MediaSessionCompat mediaSession) {
|
||||
super(mediaSession);
|
||||
this.queueDataAdapter = queueDataAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaDescriptionCompat getMediaDescription(Player player, int windowIndex) {
|
||||
return queueDataAdapter.getMediaDescription(windowIndex);
|
||||
public @NonNull MediaDescriptionCompat getMediaDescription(@NonNull Player player, int windowIndex) {
|
||||
MediaItem mediaItem = windowIndex >= 0 && windowIndex < player.getMediaItemCount() ? player.getMediaItemAt(windowIndex) : null;
|
||||
|
||||
if (mediaItem == null || mediaItem.playbackProperties == null) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
MediaDescriptionCompat mediaDescriptionCompat = (MediaDescriptionCompat) mediaItem.playbackProperties.tag;
|
||||
if (mediaDescriptionCompat == null) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return mediaDescriptionCompat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
import android.graphics.Point;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.VideoFrame;
|
||||
import org.webrtc.VideoSink;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
@@ -20,17 +19,19 @@ import java.util.WeakHashMap;
|
||||
*/
|
||||
public class BroadcastVideoSink implements VideoSink {
|
||||
|
||||
private final EglBase eglBase;
|
||||
public static final int DEVICE_ROTATION_IGNORE = -1;
|
||||
|
||||
private final EglBaseWrapper eglBase;
|
||||
private final WeakHashMap<VideoSink, Boolean> sinks;
|
||||
private final WeakHashMap<Object, Point> requestingSizes;
|
||||
private boolean dirtySizes;
|
||||
private int deviceOrientationDegrees;
|
||||
private boolean rotateToRightSide;
|
||||
private boolean forceRotate;
|
||||
private boolean rotateWithDevice;
|
||||
private RequestedSize currentlyRequestedMaxSize;
|
||||
|
||||
public BroadcastVideoSink() {
|
||||
this(null, false, true, 0);
|
||||
this(new EglBaseWrapper(null), false, true, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,18 +40,17 @@ public class BroadcastVideoSink implements VideoSink {
|
||||
* @param rotateWithDevice Rotate video frame to match device orientation
|
||||
* @param deviceOrientationDegrees Device orientation in degrees
|
||||
*/
|
||||
public BroadcastVideoSink(@Nullable EglBase eglBase, boolean forceRotate, boolean rotateWithDevice, int deviceOrientationDegrees) {
|
||||
public BroadcastVideoSink(@NonNull EglBaseWrapper eglBase, boolean forceRotate, boolean rotateWithDevice, int deviceOrientationDegrees) {
|
||||
this.eglBase = eglBase;
|
||||
this.sinks = new WeakHashMap<>();
|
||||
this.requestingSizes = new WeakHashMap<>();
|
||||
this.dirtySizes = true;
|
||||
this.deviceOrientationDegrees = deviceOrientationDegrees;
|
||||
this.rotateToRightSide = false;
|
||||
this.forceRotate = forceRotate;
|
||||
this.rotateWithDevice = rotateWithDevice;
|
||||
}
|
||||
|
||||
public @Nullable EglBase getEglBase() {
|
||||
public @NonNull EglBaseWrapper getLockableEglBase() {
|
||||
return eglBase;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,10 @@ public class BroadcastVideoSink implements VideoSink {
|
||||
|
||||
@Override
|
||||
public synchronized void onFrame(@NonNull VideoFrame videoFrame) {
|
||||
if (videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth() || forceRotate) {
|
||||
boolean isDeviceRotationIgnored = deviceOrientationDegrees == DEVICE_ROTATION_IGNORE;
|
||||
boolean isWideVideoFrame = videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth();
|
||||
|
||||
if (!isDeviceRotationIgnored && (isWideVideoFrame || forceRotate)) {
|
||||
int rotation = calculateRotation();
|
||||
if (rotation > 0) {
|
||||
rotation += rotateWithDevice ? videoFrame.getRotation() : 0;
|
||||
@@ -115,16 +118,18 @@ public class BroadcastVideoSink implements VideoSink {
|
||||
}
|
||||
|
||||
void putRequestingSize(@NonNull Object object, @NonNull Point size) {
|
||||
if (size.x == 0 || size.y == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (requestingSizes) {
|
||||
requestingSizes.put(object, size);
|
||||
dirtySizes = true;
|
||||
}
|
||||
}
|
||||
|
||||
void removeRequestingSize(@NonNull Object object) {
|
||||
synchronized (requestingSizes) {
|
||||
requestingSizes.remove(object);
|
||||
dirtySizes = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,15 +149,15 @@ public class BroadcastVideoSink implements VideoSink {
|
||||
return new RequestedSize(width, height);
|
||||
}
|
||||
|
||||
public void newSizeRequested() {
|
||||
dirtySizes = false;
|
||||
public void setCurrentlyRequestedMaxSize(@NonNull RequestedSize currentlyRequestedMaxSize) {
|
||||
this.currentlyRequestedMaxSize = currentlyRequestedMaxSize;
|
||||
}
|
||||
|
||||
public boolean needsNewRequestingSize() {
|
||||
return dirtySizes;
|
||||
return !getMaxRequestingSize().equals(currentlyRequestedMaxSize);
|
||||
}
|
||||
|
||||
public static class RequestedSize {
|
||||
public static final class RequestedSize {
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
@@ -168,5 +173,19 @@ public class BroadcastVideoSink implements VideoSink {
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
final RequestedSize that = (RequestedSize) o;
|
||||
return width == that.width && height == that.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@ public class CallParticipantView extends ConstraintLayout {
|
||||
renderer.setScalingType(scalingType);
|
||||
}
|
||||
|
||||
void setScalingType(@NonNull RendererCommon.ScalingType scalingTypeMatchOrientation, @NonNull RendererCommon.ScalingType scalingTypeMismatchOrientation) {
|
||||
renderer.setScalingType(scalingTypeMatchOrientation, scalingTypeMismatchOrientation);
|
||||
}
|
||||
|
||||
void setCallParticipant(@NonNull CallParticipant participant) {
|
||||
boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId());
|
||||
recipientId = participant.getRecipient().getId();
|
||||
@@ -140,9 +144,9 @@ public class CallParticipantView extends ConstraintLayout {
|
||||
renderer.setVisibility(hasContentToRender ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (participant.isVideoEnabled()) {
|
||||
if (participant.getVideoSink().getEglBase() != null) {
|
||||
renderer.init(participant.getVideoSink().getEglBase());
|
||||
}
|
||||
participant.getVideoSink().getLockableEglBase().performWithValidEglBase(eglBase -> {
|
||||
renderer.init(eglBase);
|
||||
});
|
||||
renderer.attachBroadcastVideoSink(participant.getVideoSink());
|
||||
} else {
|
||||
renderer.attachBroadcastVideoSink(null);
|
||||
@@ -192,6 +196,14 @@ public class CallParticipantView extends ConstraintLayout {
|
||||
pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
void hideAvatar() {
|
||||
avatar.setAlpha(0f);
|
||||
}
|
||||
|
||||
void showAvatar() {
|
||||
avatar.setAlpha(1f);
|
||||
}
|
||||
|
||||
void useLargeAvatar() {
|
||||
changeAvatarParams(LARGE_AVATAR);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ public class CallParticipantsLayout extends FlexboxLayout {
|
||||
private CallParticipant focusedParticipant = null;
|
||||
private boolean shouldRenderInPip;
|
||||
private boolean isPortrait;
|
||||
private boolean isIncomingRing;
|
||||
private LayoutStrategy layoutStrategy;
|
||||
|
||||
public CallParticipantsLayout(@NonNull Context context) {
|
||||
super(context);
|
||||
@@ -47,11 +49,21 @@ public class CallParticipantsLayout extends FlexboxLayout {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
void update(@NonNull List<CallParticipant> callParticipants, @NonNull CallParticipant focusedParticipant, boolean shouldRenderInPip, boolean isPortrait) {
|
||||
void update(@NonNull List<CallParticipant> callParticipants,
|
||||
@NonNull CallParticipant focusedParticipant,
|
||||
boolean shouldRenderInPip,
|
||||
boolean isPortrait,
|
||||
boolean isIncomingRing,
|
||||
@NonNull LayoutStrategy layoutStrategy)
|
||||
{
|
||||
this.callParticipants = callParticipants;
|
||||
this.focusedParticipant = focusedParticipant;
|
||||
this.shouldRenderInPip = shouldRenderInPip;
|
||||
this.isPortrait = isPortrait;
|
||||
this.isIncomingRing = isIncomingRing;
|
||||
this.layoutStrategy = layoutStrategy;
|
||||
|
||||
setFlexDirection(layoutStrategy.getFlexDirection());
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
@@ -107,11 +119,7 @@ public class CallParticipantsLayout extends FlexboxLayout {
|
||||
|
||||
callParticipantView.setCallParticipant(participant);
|
||||
callParticipantView.setRenderInPip(shouldRenderInPip);
|
||||
if (participant.isScreenSharing()) {
|
||||
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
|
||||
} else {
|
||||
callParticipantView.setScalingType(isPortrait || count < 3 ? RendererCommon.ScalingType.SCALE_ASPECT_FILL : RendererCommon.ScalingType.SCALE_ASPECT_BALANCED);
|
||||
}
|
||||
layoutStrategy.setChildScaling(participant, callParticipantView, isPortrait, count);
|
||||
|
||||
if (count > 1) {
|
||||
view.setPadding(MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING);
|
||||
@@ -121,13 +129,19 @@ public class CallParticipantsLayout extends FlexboxLayout {
|
||||
cardView.setRadius(0);
|
||||
}
|
||||
|
||||
if (isIncomingRing) {
|
||||
callParticipantView.hideAvatar();
|
||||
} else {
|
||||
callParticipantView.showAvatar();
|
||||
}
|
||||
|
||||
if (count > 2) {
|
||||
callParticipantView.useSmallAvatar();
|
||||
} else {
|
||||
callParticipantView.useLargeAvatar();
|
||||
}
|
||||
|
||||
setChildLayoutParams(view, index, getChildCount());
|
||||
layoutStrategy.setChildLayoutParams(view, index, getChildCount());
|
||||
}
|
||||
|
||||
private void addCallParticipantView() {
|
||||
@@ -139,17 +153,14 @@ public class CallParticipantsLayout extends FlexboxLayout {
|
||||
addView(view);
|
||||
}
|
||||
|
||||
private void setChildLayoutParams(@NonNull View child, int childPosition, int childCount) {
|
||||
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) child.getLayoutParams();
|
||||
if (childCount < 3) {
|
||||
params.setFlexBasisPercent(1f);
|
||||
} else {
|
||||
if ((childCount % 2) != 0 && childPosition == childCount - 1) {
|
||||
params.setFlexBasisPercent(1f);
|
||||
} else {
|
||||
params.setFlexBasisPercent(0.5f);
|
||||
}
|
||||
}
|
||||
child.setLayoutParams(params);
|
||||
public interface LayoutStrategy {
|
||||
int getFlexDirection();
|
||||
|
||||
void setChildScaling(@NonNull CallParticipant callParticipant,
|
||||
@NonNull CallParticipantView callParticipantView,
|
||||
boolean isPortrait,
|
||||
int childCount);
|
||||
|
||||
void setChildLayoutParams(@NonNull View child, int childPosition, int childCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import android.view.View
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.webrtc.RendererCommon
|
||||
|
||||
object CallParticipantsLayoutStrategies {
|
||||
|
||||
private object Portrait : CallParticipantsLayout.LayoutStrategy {
|
||||
override fun getFlexDirection(): Int = FlexDirection.ROW
|
||||
|
||||
override fun setChildScaling(callParticipant: CallParticipant, callParticipantView: CallParticipantView, isPortrait: Boolean, childCount: Int) {
|
||||
if (callParticipant.isScreenSharing) {
|
||||
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
} else {
|
||||
callParticipantView.setScalingType(if (isPortrait || childCount < 3) RendererCommon.ScalingType.SCALE_ASPECT_FILL else RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setChildLayoutParams(child: View, childPosition: Int, childCount: Int) {
|
||||
val params = child.layoutParams as FlexboxLayout.LayoutParams
|
||||
if (childCount < 3) {
|
||||
params.flexBasisPercent = 1f
|
||||
} else {
|
||||
if ((childCount % 2) != 0 && childPosition == childCount - 1) {
|
||||
params.flexBasisPercent = 1f
|
||||
} else {
|
||||
params.flexBasisPercent = 0.5f
|
||||
}
|
||||
}
|
||||
child.layoutParams = params
|
||||
}
|
||||
}
|
||||
|
||||
private object Landscape : CallParticipantsLayout.LayoutStrategy {
|
||||
override fun getFlexDirection() = FlexDirection.COLUMN
|
||||
|
||||
override fun setChildScaling(callParticipant: CallParticipant, callParticipantView: CallParticipantView, isPortrait: Boolean, childCount: Int) {
|
||||
if (callParticipant.isScreenSharing) {
|
||||
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
} else {
|
||||
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL, RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setChildLayoutParams(child: View, childPosition: Int, childCount: Int) {
|
||||
val params = child.layoutParams as FlexboxLayout.LayoutParams
|
||||
if (childCount < 4) {
|
||||
params.flexBasisPercent = 1f
|
||||
} else {
|
||||
if ((childCount % 2) != 0 && childPosition == childCount - 1) {
|
||||
params.flexBasisPercent = 1f
|
||||
} else {
|
||||
params.flexBasisPercent = 0.5f
|
||||
}
|
||||
}
|
||||
child.layoutParams = params
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getStrategy(isPortrait: Boolean, isLandscapeEnabled: Boolean): CallParticipantsLayout.LayoutStrategy {
|
||||
return if (isPortrait || !isLandscapeEnabled) {
|
||||
Portrait
|
||||
} else {
|
||||
Landscape
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.ComparatorCompat;
|
||||
import com.annimon.stream.OptionalLong;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the state of all participants, remote and local, combined with view state
|
||||
* needed to properly render the participants. The view state primarily consists of
|
||||
* if we are in System PIP mode and if we should show our video for an outgoing call.
|
||||
*/
|
||||
public final class CallParticipantsState {
|
||||
|
||||
private static final int SMALL_GROUP_MAX = 6;
|
||||
|
||||
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
|
||||
WebRtcViewModel.GroupCallState.IDLE,
|
||||
new ParticipantCollection(SMALL_GROUP_MAX),
|
||||
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(), false),
|
||||
CallParticipant.EMPTY,
|
||||
WebRtcLocalRenderState.GONE,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
OptionalLong.empty());
|
||||
|
||||
private final WebRtcViewModel.State callState;
|
||||
private final WebRtcViewModel.GroupCallState groupCallState;
|
||||
private final ParticipantCollection remoteParticipants;
|
||||
private final CallParticipant localParticipant;
|
||||
private final CallParticipant focusedParticipant;
|
||||
private final WebRtcLocalRenderState localRenderState;
|
||||
private final boolean isInPipMode;
|
||||
private final boolean showVideoForOutgoing;
|
||||
private final boolean isViewingFocusedParticipant;
|
||||
private final OptionalLong remoteDevicesCount;
|
||||
|
||||
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
|
||||
@NonNull WebRtcViewModel.GroupCallState groupCallState,
|
||||
@NonNull ParticipantCollection remoteParticipants,
|
||||
@NonNull CallParticipant localParticipant,
|
||||
@NonNull CallParticipant focusedParticipant,
|
||||
@NonNull WebRtcLocalRenderState localRenderState,
|
||||
boolean isInPipMode,
|
||||
boolean showVideoForOutgoing,
|
||||
boolean isViewingFocusedParticipant,
|
||||
OptionalLong remoteDevicesCount)
|
||||
{
|
||||
this.callState = callState;
|
||||
this.groupCallState = groupCallState;
|
||||
this.remoteParticipants = remoteParticipants;
|
||||
this.localParticipant = localParticipant;
|
||||
this.localRenderState = localRenderState;
|
||||
this.focusedParticipant = focusedParticipant;
|
||||
this.isInPipMode = isInPipMode;
|
||||
this.showVideoForOutgoing = showVideoForOutgoing;
|
||||
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
|
||||
this.remoteDevicesCount = remoteDevicesCount;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.State getCallState() {
|
||||
return callState;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
|
||||
return groupCallState;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getGridParticipants() {
|
||||
return remoteParticipants.getGridParticipants();
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getListParticipants() {
|
||||
List<CallParticipant> listParticipants = new ArrayList<>();
|
||||
|
||||
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
|
||||
listParticipants.addAll(getAllRemoteParticipants());
|
||||
listParticipants.remove(focusedParticipant);
|
||||
} else {
|
||||
listParticipants.addAll(remoteParticipants.getListParticipants());
|
||||
}
|
||||
|
||||
listParticipants.add(CallParticipant.EMPTY);
|
||||
Collections.reverse(listParticipants);
|
||||
|
||||
return listParticipants;
|
||||
}
|
||||
|
||||
public @NonNull String getRemoteParticipantsDescription(@NonNull Context context) {
|
||||
switch (remoteParticipants.size()) {
|
||||
case 0:
|
||||
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
|
||||
case 1: {
|
||||
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
return remoteParticipants.get(0).getRecipientDisplayName(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 2: {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
|
||||
remoteParticipants.get(0).getShortRecipientDisplayName(context),
|
||||
remoteParticipants.get(1).getShortRecipientDisplayName(context));
|
||||
}
|
||||
}
|
||||
default: {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
int others = remoteParticipants.size() - 2;
|
||||
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
|
||||
others,
|
||||
remoteParticipants.get(0).getShortRecipientDisplayName(context),
|
||||
remoteParticipants.get(1).getShortRecipientDisplayName(context),
|
||||
others);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
|
||||
return remoteParticipants.getAllParticipants();
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant getLocalParticipant() {
|
||||
return localParticipant;
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant getFocusedParticipant() {
|
||||
return focusedParticipant;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcLocalRenderState getLocalRenderState() {
|
||||
return localRenderState;
|
||||
}
|
||||
|
||||
public boolean isLargeVideoGroup() {
|
||||
return getAllRemoteParticipants().size() > SMALL_GROUP_MAX;
|
||||
}
|
||||
|
||||
public boolean isInPipMode() {
|
||||
return isInPipMode;
|
||||
}
|
||||
|
||||
public boolean isViewingFocusedParticipant() {
|
||||
return isViewingFocusedParticipant;
|
||||
}
|
||||
|
||||
public boolean needsNewRequestSizes() {
|
||||
if (groupCallState.isNotIdle()) {
|
||||
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull OptionalLong getRemoteDevicesCount() {
|
||||
return remoteDevicesCount;
|
||||
}
|
||||
|
||||
public @NonNull OptionalLong getParticipantCount() {
|
||||
boolean includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
|
||||
|
||||
return remoteDevicesCount.map(l -> l + (includeSelf ? 1L : 0L))
|
||||
.or(() -> includeSelf ? OptionalLong.of(1L) : OptionalLong.empty());
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
|
||||
@NonNull WebRtcViewModel webRtcViewModel,
|
||||
boolean enableVideo)
|
||||
{
|
||||
boolean newShowVideoForOutgoing = oldState.showVideoForOutgoing;
|
||||
if (enableVideo) {
|
||||
newShowVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
newShowVideoForOutgoing = false;
|
||||
}
|
||||
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(webRtcViewModel.getLocalParticipant(),
|
||||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
webRtcViewModel.getGroupState().isNotIdle(),
|
||||
webRtcViewModel.getState(),
|
||||
webRtcViewModel.getRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getGroupState(),
|
||||
oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()),
|
||||
webRtcViewModel.getLocalParticipant(),
|
||||
getFocusedParticipant(webRtcViewModel.getRemoteParticipants()),
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
webRtcViewModel.getRemoteDevicesCount());
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
isInPip,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
isInPip,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState setExpanded(@NonNull CallParticipantsState oldState, boolean expanded) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
expanded);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
selectedPage == SelectedPage.FOCUSED,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
selectedPage == SelectedPage.FOCUSED,
|
||||
oldState.remoteDevicesCount);
|
||||
}
|
||||
|
||||
private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
|
||||
boolean isInPip,
|
||||
boolean showVideoForOutgoing,
|
||||
boolean isNonIdleGroupCall,
|
||||
@NonNull WebRtcViewModel.State callState,
|
||||
int numberOfRemoteParticipants,
|
||||
boolean isViewingFocusedParticipant,
|
||||
boolean isExpanded)
|
||||
{
|
||||
boolean displayLocal = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled());
|
||||
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
|
||||
|
||||
if (isExpanded && (localParticipant.isVideoEnabled() || isNonIdleGroupCall)) {
|
||||
return WebRtcLocalRenderState.EXPANDED;
|
||||
} else if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
|
||||
} else if (numberOfRemoteParticipants == 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
|
||||
} else {
|
||||
localRenderState = localParticipant.isVideoEnabled() ? WebRtcLocalRenderState.LARGE : WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
localRenderState = localParticipant.isVideoEnabled() ? WebRtcLocalRenderState.LARGE : WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
|
||||
return localRenderState;
|
||||
}
|
||||
|
||||
private static @NonNull CallParticipant getFocusedParticipant(@NonNull List<CallParticipant> participants) {
|
||||
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(participants);
|
||||
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
|
||||
|
||||
return participantsByLastSpoke.isEmpty() ? CallParticipant.EMPTY
|
||||
: participantsByLastSpoke.stream()
|
||||
.filter(CallParticipant::isScreenSharing)
|
||||
.findAny().orElse(participantsByLastSpoke.get(0));
|
||||
}
|
||||
|
||||
public enum SelectedPage {
|
||||
GRID,
|
||||
FOCUSED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.annimon.stream.OptionalLong
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls.FoldableState
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.CallParticipant.Companion.createLocal
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState
|
||||
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Represents the state of all participants, remote and local, combined with view state
|
||||
* needed to properly render the participants. The view state primarily consists of
|
||||
* if we are in System PIP mode and if we should show our video for an outgoing call.
|
||||
*/
|
||||
data class CallParticipantsState(
|
||||
val callState: WebRtcViewModel.State = WebRtcViewModel.State.CALL_DISCONNECTED,
|
||||
val groupCallState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE,
|
||||
private val remoteParticipants: ParticipantCollection = ParticipantCollection(SMALL_GROUP_MAX),
|
||||
val localParticipant: CallParticipant = createLocal(CameraState.UNKNOWN, BroadcastVideoSink(), false),
|
||||
val focusedParticipant: CallParticipant = CallParticipant.EMPTY,
|
||||
val localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE,
|
||||
val isInPipMode: Boolean = false,
|
||||
private val showVideoForOutgoing: Boolean = false,
|
||||
val isViewingFocusedParticipant: Boolean = false,
|
||||
val remoteDevicesCount: OptionalLong = OptionalLong.empty(),
|
||||
private val foldableState: FoldableState = FoldableState.flat(),
|
||||
val isInOutgoingRingingMode: Boolean = false,
|
||||
val ringGroup: Boolean = false,
|
||||
val ringerRecipient: Recipient = Recipient.UNKNOWN,
|
||||
val groupMembers: List<GroupMemberEntry.FullMember> = emptyList()
|
||||
) {
|
||||
|
||||
val allRemoteParticipants: List<CallParticipant> = remoteParticipants.allParticipants
|
||||
val isFolded: Boolean = foldableState.isFolded
|
||||
val isLargeVideoGroup: Boolean = allRemoteParticipants.size > SMALL_GROUP_MAX
|
||||
val isIncomingRing: Boolean = callState == WebRtcViewModel.State.CALL_INCOMING
|
||||
|
||||
val gridParticipants: List<CallParticipant>
|
||||
get() {
|
||||
return remoteParticipants.gridParticipants
|
||||
}
|
||||
|
||||
val listParticipants: List<CallParticipant>
|
||||
get() {
|
||||
val listParticipants: MutableList<CallParticipant> = mutableListOf()
|
||||
if (isViewingFocusedParticipant && allRemoteParticipants.size > 1) {
|
||||
listParticipants.addAll(allRemoteParticipants)
|
||||
listParticipants.remove(focusedParticipant)
|
||||
} else {
|
||||
listParticipants.addAll(remoteParticipants.listParticipants)
|
||||
}
|
||||
if (foldableState.isFlat) {
|
||||
listParticipants.add(CallParticipant.EMPTY)
|
||||
}
|
||||
listParticipants.reverse()
|
||||
return listParticipants
|
||||
}
|
||||
|
||||
val participantCount: OptionalLong
|
||||
get() {
|
||||
val includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED
|
||||
return remoteDevicesCount.map { l: Long -> l + if (includeSelf) 1L else 0L }
|
||||
.or { if (includeSelf) OptionalLong.of(1L) else OptionalLong.empty() }
|
||||
}
|
||||
|
||||
fun getPreJoinGroupDescription(context: Context): String? {
|
||||
if (callState != WebRtcViewModel.State.CALL_PRE_JOIN || groupCallState.isIdle) {
|
||||
return null
|
||||
}
|
||||
|
||||
return if (remoteParticipants.isEmpty) {
|
||||
describeGroupMembers(
|
||||
context = context,
|
||||
oneParticipant = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s else R.string.WebRtcCallView__s_will_be_notified,
|
||||
twoParticipants = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s_and_s else R.string.WebRtcCallView__s_and_s_will_be_notified,
|
||||
multipleParticipants = if (ringGroup) R.plurals.WebRtcCallView__signal_will_ring_s_s_and_d_others else R.plurals.WebRtcCallView__s_s_and_d_others_will_be_notified,
|
||||
members = groupMembers
|
||||
)
|
||||
} else {
|
||||
when (remoteParticipants.size()) {
|
||||
0 -> context.getString(R.string.WebRtcCallView__no_one_else_is_here)
|
||||
1 -> context.getString(if (remoteParticipants[0].isSelf) R.string.WebRtcCallView__s_are_in_this_call else R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants[0].getShortRecipientDisplayName(context))
|
||||
2 -> context.getString(
|
||||
R.string.WebRtcCallView__s_and_s_are_in_this_call,
|
||||
remoteParticipants[0].getShortRecipientDisplayName(context),
|
||||
remoteParticipants[1].getShortRecipientDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = remoteParticipants.size() - 2
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
|
||||
others,
|
||||
remoteParticipants[0].getShortRecipientDisplayName(context),
|
||||
remoteParticipants[1].getShortRecipientDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getOutgoingRingingGroupDescription(context: Context): String? {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED &&
|
||||
groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED &&
|
||||
isInOutgoingRingingMode
|
||||
) {
|
||||
return describeGroupMembers(
|
||||
context = context,
|
||||
oneParticipant = R.string.WebRtcCallView__ringing_s,
|
||||
twoParticipants = R.string.WebRtcCallView__ringing_s_and_s,
|
||||
multipleParticipants = R.plurals.WebRtcCallView__ringing_s_s_and_d_others,
|
||||
members = groupMembers
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun getIncomingRingingGroupDescription(context: Context): String? {
|
||||
if (callState == WebRtcViewModel.State.CALL_INCOMING &&
|
||||
groupCallState == WebRtcViewModel.GroupCallState.RINGING &&
|
||||
ringerRecipient.hasUuid()
|
||||
) {
|
||||
val ringerName = ringerRecipient.getShortDisplayName(context)
|
||||
val membersWithoutYouOrRinger: List<GroupMemberEntry.FullMember> = groupMembers.filterNot { it.member.isSelf || ringerRecipient.requireUuid() == it.member.uuid.orNull() }
|
||||
|
||||
return when (membersWithoutYouOrRinger.size) {
|
||||
0 -> context.getString(R.string.WebRtcCallView__s_is_calling_you, ringerName)
|
||||
1 -> context.getString(
|
||||
R.string.WebRtcCallView__s_is_calling_you_and_s,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context)
|
||||
)
|
||||
2 -> context.getString(
|
||||
R.string.WebRtcCallView__s_is_calling_you_s_and_s,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context),
|
||||
membersWithoutYouOrRinger[1].member.getShortDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = membersWithoutYouOrRinger.size - 2
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.WebRtcCallView__s_is_calling_you_s_s_and_d_others,
|
||||
others,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context),
|
||||
membersWithoutYouOrRinger[1].member.getShortDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun needsNewRequestSizes(): Boolean {
|
||||
return if (groupCallState.isNotIdle) {
|
||||
allRemoteParticipants.any { it.videoSink.needsNewRequestingSize() }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SMALL_GROUP_MAX = 6
|
||||
|
||||
@JvmField
|
||||
val MAX_OUTGOING_GROUP_RING_DURATION = TimeUnit.MINUTES.toMillis(1)
|
||||
|
||||
@JvmField
|
||||
val STARTING_STATE = CallParticipantsState()
|
||||
|
||||
@JvmStatic
|
||||
fun update(
|
||||
oldState: CallParticipantsState,
|
||||
webRtcViewModel: WebRtcViewModel,
|
||||
enableVideo: Boolean
|
||||
): CallParticipantsState {
|
||||
|
||||
var newShowVideoForOutgoing: Boolean = oldState.showVideoForOutgoing
|
||||
if (enableVideo) {
|
||||
newShowVideoForOutgoing = webRtcViewModel.state == WebRtcViewModel.State.CALL_OUTGOING
|
||||
} else if (webRtcViewModel.state != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
newShowVideoForOutgoing = false
|
||||
}
|
||||
|
||||
val isInOutgoingRingingMode = if (oldState.isInOutgoingRingingMode) {
|
||||
webRtcViewModel.callConnectedTime + MAX_OUTGOING_GROUP_RING_DURATION > System.currentTimeMillis() && webRtcViewModel.remoteParticipants.size == 0
|
||||
} else {
|
||||
oldState.ringGroup &&
|
||||
webRtcViewModel.callConnectedTime + MAX_OUTGOING_GROUP_RING_DURATION > System.currentTimeMillis() &&
|
||||
webRtcViewModel.remoteParticipants.size == 0 &&
|
||||
oldState.callState == WebRtcViewModel.State.CALL_OUTGOING &&
|
||||
webRtcViewModel.state == WebRtcViewModel.State.CALL_CONNECTED
|
||||
}
|
||||
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(
|
||||
oldState = oldState,
|
||||
localParticipant = webRtcViewModel.localParticipant,
|
||||
showVideoForOutgoing = newShowVideoForOutgoing,
|
||||
isNonIdleGroupCall = webRtcViewModel.groupState.isNotIdle,
|
||||
callState = webRtcViewModel.state,
|
||||
numberOfRemoteParticipants = webRtcViewModel.remoteParticipants.size
|
||||
)
|
||||
|
||||
return oldState.copy(
|
||||
callState = webRtcViewModel.state,
|
||||
groupCallState = webRtcViewModel.groupState,
|
||||
remoteParticipants = oldState.remoteParticipants.getNext(webRtcViewModel.remoteParticipants),
|
||||
localParticipant = webRtcViewModel.localParticipant,
|
||||
focusedParticipant = getFocusedParticipant(webRtcViewModel.remoteParticipants),
|
||||
localRenderState = localRenderState,
|
||||
showVideoForOutgoing = newShowVideoForOutgoing,
|
||||
remoteDevicesCount = webRtcViewModel.remoteDevicesCount,
|
||||
ringGroup = webRtcViewModel.shouldRingGroup(),
|
||||
isInOutgoingRingingMode = isInOutgoingRingingMode,
|
||||
ringerRecipient = webRtcViewModel.ringerRecipient
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, isInPip: Boolean): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isInPip = isInPip)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, isInPipMode = isInPip)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setExpanded(oldState: CallParticipantsState, expanded: Boolean): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isExpanded = expanded)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, selectedPage: SelectedPage): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isViewingFocusedParticipant = selectedPage == SelectedPage.FOCUSED)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, isViewingFocusedParticipant = selectedPage == SelectedPage.FOCUSED)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, foldableState: FoldableState): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, foldableState = foldableState)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, groupMembers: List<GroupMemberEntry.FullMember>): CallParticipantsState {
|
||||
return oldState.copy(groupMembers = groupMembers)
|
||||
}
|
||||
|
||||
private fun determineLocalRenderMode(
|
||||
oldState: CallParticipantsState,
|
||||
localParticipant: CallParticipant = oldState.localParticipant,
|
||||
isInPip: Boolean = oldState.isInPipMode,
|
||||
showVideoForOutgoing: Boolean = oldState.showVideoForOutgoing,
|
||||
isNonIdleGroupCall: Boolean = oldState.groupCallState.isNotIdle,
|
||||
callState: WebRtcViewModel.State = oldState.callState,
|
||||
numberOfRemoteParticipants: Int = oldState.allRemoteParticipants.size,
|
||||
isViewingFocusedParticipant: Boolean = oldState.isViewingFocusedParticipant,
|
||||
isExpanded: Boolean = oldState.localRenderState == WebRtcLocalRenderState.EXPANDED
|
||||
): WebRtcLocalRenderState {
|
||||
|
||||
val displayLocal: Boolean = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled)
|
||||
var localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE
|
||||
|
||||
if (isExpanded && (localParticipant.isVideoEnabled || isNonIdleGroupCall)) {
|
||||
return WebRtcLocalRenderState.EXPANDED
|
||||
} else if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
localRenderState = if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
WebRtcLocalRenderState.SMALLER_RECTANGLE
|
||||
} else if (numberOfRemoteParticipants == 1) {
|
||||
WebRtcLocalRenderState.SMALL_RECTANGLE
|
||||
} else {
|
||||
if (localParticipant.isVideoEnabled) WebRtcLocalRenderState.LARGE else WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
localRenderState = if (localParticipant.isVideoEnabled) WebRtcLocalRenderState.LARGE else WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
return localRenderState
|
||||
}
|
||||
|
||||
private fun getFocusedParticipant(participants: List<CallParticipant>): CallParticipant {
|
||||
val participantsByLastSpoke: List<CallParticipant> = participants.sortedByDescending(CallParticipant::lastSpoke)
|
||||
|
||||
return if (participantsByLastSpoke.isEmpty()) {
|
||||
CallParticipant.EMPTY
|
||||
} else {
|
||||
participantsByLastSpoke.firstOrNull(CallParticipant::isScreenSharing) ?: participantsByLastSpoke[0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun describeGroupMembers(
|
||||
context: Context,
|
||||
@StringRes oneParticipant: Int,
|
||||
@StringRes twoParticipants: Int,
|
||||
@PluralsRes multipleParticipants: Int,
|
||||
members: List<GroupMemberEntry.FullMember>
|
||||
): String {
|
||||
val membersWithoutYou: List<GroupMemberEntry.FullMember> = members.filterNot { it.member.isSelf }
|
||||
|
||||
return when (membersWithoutYou.size) {
|
||||
0 -> ""
|
||||
1 -> context.getString(
|
||||
oneParticipant,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context)
|
||||
)
|
||||
2 -> context.getString(
|
||||
twoParticipants,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context),
|
||||
membersWithoutYou[1].member.getShortDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = membersWithoutYou.size - 2
|
||||
context.resources.getQuantityString(
|
||||
multipleParticipants,
|
||||
others,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context),
|
||||
membersWithoutYou[1].member.getShortDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SelectedPage {
|
||||
GRID, FOCUSED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import android.opengl.EGL14
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.EglBase10
|
||||
import org.webrtc.EglBase14
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.Consumer
|
||||
import javax.microedition.khronos.egl.EGL10
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
private val TAG = Log.tag(EglBaseWrapper::class.java)
|
||||
|
||||
/**
|
||||
* Wrapper which allows caller to perform synchronized actions on an EglBase object.
|
||||
*/
|
||||
class EglBaseWrapper(val eglBase: EglBase?) {
|
||||
|
||||
private val lock: Lock = ReentrantLock()
|
||||
|
||||
fun require(): EglBase = requireNotNull(eglBase)
|
||||
|
||||
@Volatile
|
||||
private var isReleased: Boolean = false
|
||||
|
||||
fun performWithValidEglBase(consumer: Consumer<EglBase>) {
|
||||
if (isReleased) {
|
||||
Log.d(TAG, "Tried to use a released EglBase", Exception())
|
||||
return
|
||||
}
|
||||
|
||||
if (eglBase == null) {
|
||||
return
|
||||
}
|
||||
|
||||
lock.withLock {
|
||||
if (isReleased) {
|
||||
Log.d(TAG, "Tried to use a released EglBase", Exception())
|
||||
return
|
||||
}
|
||||
|
||||
val hasSharedContext = when (val context: EglBase.Context = eglBase.eglBaseContext) {
|
||||
is EglBase14.Context -> context.rawContext != EGL14.EGL_NO_CONTEXT
|
||||
is EglBase10.Context -> context.rawContext != EGL10.EGL_NO_CONTEXT
|
||||
else -> throw IllegalStateException("Unknown context")
|
||||
}
|
||||
|
||||
if (hasSharedContext) {
|
||||
consumer.accept(eglBase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun releaseEglBase() {
|
||||
if (isReleased || eglBase == null) {
|
||||
return
|
||||
}
|
||||
|
||||
lock.withLock {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
|
||||
isReleased = true
|
||||
eglBase.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,14 +116,16 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
}
|
||||
|
||||
public void clearVerticalBoundaries() {
|
||||
setVerticalBoundaries(parent.getTop(), parent.getMeasuredHeight() + parent.getTop());
|
||||
setTopVerticalBoundary(parent.getTop());
|
||||
setBottomVerticalBoundary(parent.getMeasuredHeight() + parent.getTop());
|
||||
}
|
||||
|
||||
public void setVerticalBoundaries(int topBoundary, int bottomBoundary) {
|
||||
extraPaddingTop = topBoundary - parent.getTop();
|
||||
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
|
||||
public void setTopVerticalBoundary(int topBoundary) {
|
||||
extraPaddingTop = topBoundary - parent.getTop();
|
||||
}
|
||||
|
||||
adjustPip();
|
||||
public void setBottomVerticalBoundary(int bottomBoundary) {
|
||||
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
|
||||
}
|
||||
|
||||
private boolean onGestureFinished(MotionEvent e) {
|
||||
|
||||
@@ -187,8 +187,6 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
|
||||
setMeasuredDimension(size.x, size.y);
|
||||
|
||||
Log.d(TAG, "onMeasure(). New size: " + size.x + "x" + size.y);
|
||||
|
||||
if (attachedVideoSink != null) {
|
||||
attachedVideoSink.putRequestingSize(this, size);
|
||||
}
|
||||
|
||||
@@ -15,33 +15,42 @@ class WebRtcCallParticipantsPage {
|
||||
private final boolean isSpeaker;
|
||||
private final boolean isRenderInPip;
|
||||
private final boolean isPortrait;
|
||||
private final boolean isLandscapeEnabled;
|
||||
private final boolean isIncomingRing;
|
||||
|
||||
static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List<CallParticipant> callParticipants,
|
||||
@NonNull CallParticipant focusedParticipant,
|
||||
boolean isRenderInPip,
|
||||
boolean isPortrait)
|
||||
boolean isPortrait,
|
||||
boolean isLandscapeEnabled,
|
||||
boolean isIncomingRing)
|
||||
{
|
||||
return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip, isPortrait);
|
||||
return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip, isPortrait, isLandscapeEnabled, isIncomingRing);
|
||||
}
|
||||
|
||||
static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
|
||||
boolean isRenderInPip,
|
||||
boolean isPortrait)
|
||||
boolean isPortrait,
|
||||
boolean isLandscapeEnabled)
|
||||
{
|
||||
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), singleParticipant, true, isRenderInPip, isPortrait);
|
||||
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), singleParticipant, true, isRenderInPip, isPortrait, isLandscapeEnabled, false);
|
||||
}
|
||||
|
||||
private WebRtcCallParticipantsPage(@NonNull List<CallParticipant> callParticipants,
|
||||
@NonNull CallParticipant focusedParticipant,
|
||||
boolean isSpeaker,
|
||||
boolean isRenderInPip,
|
||||
boolean isPortrait)
|
||||
boolean isPortrait,
|
||||
boolean isLandscapeEnabled,
|
||||
boolean isIncomingRing)
|
||||
{
|
||||
this.callParticipants = callParticipants;
|
||||
this.focusedParticipant = focusedParticipant;
|
||||
this.isSpeaker = isSpeaker;
|
||||
this.isRenderInPip = isRenderInPip;
|
||||
this.isPortrait = isPortrait;
|
||||
this.isLandscapeEnabled = isLandscapeEnabled;
|
||||
this.isIncomingRing = isIncomingRing;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getCallParticipants() {
|
||||
@@ -64,6 +73,14 @@ class WebRtcCallParticipantsPage {
|
||||
return isPortrait;
|
||||
}
|
||||
|
||||
public boolean isIncomingRing() {
|
||||
return isIncomingRing;
|
||||
}
|
||||
|
||||
public @NonNull CallParticipantsLayout.LayoutStrategy getLayoutStrategy() {
|
||||
return CallParticipantsLayoutStrategies.getStrategy(isPortrait, isLandscapeEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -86,7 +86,7 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipa
|
||||
|
||||
@Override
|
||||
void bind(WebRtcCallParticipantsPage page) {
|
||||
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip(), page.isPortrait());
|
||||
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip(), page.isPortrait(), page.isIncomingRing(), page.getLayoutStrategy());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ class WebRtcCallRepository {
|
||||
@WorkerThread
|
||||
void getIdentityRecords(@NonNull Recipient recipient, @NonNull Consumer<IdentityRecordList> consumer) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
List<Recipient> recipients;
|
||||
List<Recipient> recipients;
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
@@ -51,7 +50,7 @@ class WebRtcCallRepository {
|
||||
recipients = Collections.singletonList(recipient);
|
||||
}
|
||||
|
||||
consumer.accept(identityDatabase.getIdentities(recipients));
|
||||
consumer.accept(ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
import android.content.Context;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
@@ -16,7 +16,6 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.constraintlayout.widget.Guideline;
|
||||
@@ -32,11 +31,12 @@ import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.ResizeAnimation;
|
||||
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
||||
import org.thoughtcrime.securesms.components.sensors.Orientation;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
@@ -47,8 +47,10 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
|
||||
import org.webrtc.RendererCommon;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
@@ -57,9 +59,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
public class WebRtcCallView extends FrameLayout {
|
||||
public class WebRtcCallView extends ConstraintLayout {
|
||||
|
||||
private static final long TRANSITION_DURATION_MILLIS = 250;
|
||||
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
|
||||
@@ -70,8 +70,11 @@ public class WebRtcCallView extends FrameLayout {
|
||||
public static final int CONTROLS_HEIGHT = 98;
|
||||
|
||||
private WebRtcAudioOutputToggleButton audioToggle;
|
||||
private TextView audioToggleLabel;
|
||||
private AccessibleToggleButton videoToggle;
|
||||
private TextView videoToggleLabel;
|
||||
private AccessibleToggleButton micToggle;
|
||||
private TextView micToggleLabel;
|
||||
private ViewGroup smallLocalRenderFrame;
|
||||
private CallParticipantView smallLocalRender;
|
||||
private View largeLocalRenderFrame;
|
||||
@@ -80,42 +83,58 @@ public class WebRtcCallView extends FrameLayout {
|
||||
private ImageView largeLocalRenderNoVideoAvatar;
|
||||
private TextView recipientName;
|
||||
private TextView status;
|
||||
private TextView incomingRingStatus;
|
||||
private ConstraintLayout parent;
|
||||
private ConstraintLayout participantsParent;
|
||||
private ControlsListener controlsListener;
|
||||
private RecipientId recipientId;
|
||||
private ImageView answer;
|
||||
private ImageView cameraDirectionToggle;
|
||||
private TextView cameraDirectionToggleLabel;
|
||||
private AccessibleToggleButton ringToggle;
|
||||
private TextView ringToggleLabel;
|
||||
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
|
||||
private ImageView hangup;
|
||||
private TextView hangupLabel;
|
||||
private View answerWithAudio;
|
||||
private View answerWithAudioLabel;
|
||||
private View topGradient;
|
||||
private View footerGradient;
|
||||
private View startCallControls;
|
||||
private ViewPager2 callParticipantsPager;
|
||||
private RecyclerView callParticipantsRecycler;
|
||||
private Toolbar toolbar;
|
||||
private ConstraintLayout toolbar;
|
||||
private MaterialButton startCall;
|
||||
private TextView participantCount;
|
||||
private Stub<FrameLayout> groupCallSpeakerHint;
|
||||
private Stub<View> groupCallFullStub;
|
||||
private View errorButton;
|
||||
private int pagerBottomMarginDp;
|
||||
private boolean controlsVisible = true;
|
||||
private Guideline showParticipantsGuideline;
|
||||
private Guideline topFoldGuideline;
|
||||
private Guideline callScreenTopFoldGuideline;
|
||||
private View foldParticipantCountWrapper;
|
||||
private TextView foldParticipantCount;
|
||||
private AvatarImageView largeHeaderAvatar;
|
||||
private ConstraintSet smallHeaderConstraints;
|
||||
private Guideline statusBarGuideline;
|
||||
private View fullScreenShade;
|
||||
|
||||
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
|
||||
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
|
||||
private PictureInPictureExpansionHelper pictureInPictureExpansionHelper;
|
||||
|
||||
|
||||
private final Set<View> incomingCallViews = new HashSet<>();
|
||||
private final Set<View> topViews = new HashSet<>();
|
||||
private final Set<View> visibleViewSet = new HashSet<>();
|
||||
private final Set<View> allTimeVisibleViews = new HashSet<>();
|
||||
private final Set<View> adjustableMarginsSet = new HashSet<>();
|
||||
private final Set<View> rotatableControls = new HashSet<>();
|
||||
|
||||
private WebRtcControls controls = WebRtcControls.NONE;
|
||||
private final Runnable fadeOutRunnable = () -> {
|
||||
|
||||
private final ThrottledDebouncer throttledDebouncer = new ThrottledDebouncer(TRANSITION_DURATION_MILLIS);
|
||||
private WebRtcControls controls = WebRtcControls.NONE;
|
||||
private final Runnable fadeOutRunnable = () -> {
|
||||
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls();
|
||||
};
|
||||
|
||||
@@ -135,8 +154,11 @@ public class WebRtcCallView extends FrameLayout {
|
||||
super.onFinishInflate();
|
||||
|
||||
audioToggle = findViewById(R.id.call_screen_speaker_toggle);
|
||||
audioToggleLabel = findViewById(R.id.call_screen_speaker_toggle_label);
|
||||
videoToggle = findViewById(R.id.call_screen_video_toggle);
|
||||
videoToggleLabel = findViewById(R.id.call_screen_video_toggle_label);
|
||||
micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
|
||||
micToggleLabel = findViewById(R.id.call_screen_audio_mic_toggle_label);
|
||||
smallLocalRenderFrame = findViewById(R.id.call_screen_pip);
|
||||
smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
|
||||
largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
|
||||
@@ -145,28 +167,40 @@ public class WebRtcCallView extends FrameLayout {
|
||||
largeLocalRenderNoVideoAvatar = findViewById(R.id.call_screen_large_local_video_off_avatar);
|
||||
recipientName = findViewById(R.id.call_screen_recipient_name);
|
||||
status = findViewById(R.id.call_screen_status);
|
||||
incomingRingStatus = findViewById(R.id.call_screen_incoming_ring_status);
|
||||
parent = findViewById(R.id.call_screen);
|
||||
participantsParent = findViewById(R.id.call_screen_participants_parent);
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
cameraDirectionToggleLabel = findViewById(R.id.call_screen_camera_direction_toggle_label);
|
||||
ringToggle = findViewById(R.id.call_screen_audio_ring_toggle);
|
||||
ringToggleLabel = findViewById(R.id.call_screen_audio_ring_toggle_label);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
hangupLabel = findViewById(R.id.call_screen_end_call_label);
|
||||
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
|
||||
topGradient = findViewById(R.id.call_screen_header_gradient);
|
||||
footerGradient = findViewById(R.id.call_screen_footer_gradient);
|
||||
startCallControls = findViewById(R.id.call_screen_start_call_controls);
|
||||
callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
|
||||
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
|
||||
toolbar = findViewById(R.id.call_screen_toolbar);
|
||||
toolbar = findViewById(R.id.call_screen_header);
|
||||
startCall = findViewById(R.id.call_screen_start_call_start_call);
|
||||
errorButton = findViewById(R.id.call_screen_error_cancel);
|
||||
groupCallSpeakerHint = new Stub<>(findViewById(R.id.call_screen_group_call_speaker_hint));
|
||||
groupCallFullStub = new Stub<>(findViewById(R.id.group_call_call_full_view));
|
||||
showParticipantsGuideline = findViewById(R.id.call_screen_show_participants_guideline);
|
||||
topFoldGuideline = findViewById(R.id.fold_top_guideline);
|
||||
callScreenTopFoldGuideline = findViewById(R.id.fold_top_call_screen_guideline);
|
||||
foldParticipantCountWrapper = findViewById(R.id.fold_show_participants_menu_counter_wrapper);
|
||||
foldParticipantCount = findViewById(R.id.fold_show_participants_menu_counter);
|
||||
largeHeaderAvatar = findViewById(R.id.call_screen_header_avatar);
|
||||
statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
|
||||
fullScreenShade = findViewById(R.id.call_screen_full_shade);
|
||||
|
||||
View topGradient = findViewById(R.id.call_screen_header_gradient);
|
||||
View decline = findViewById(R.id.call_screen_decline_call);
|
||||
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
|
||||
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
|
||||
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
|
||||
|
||||
callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
|
||||
|
||||
@@ -209,6 +243,10 @@ public class WebRtcCallView extends FrameLayout {
|
||||
runIfNonNull(controlsListener, listener -> listener.onMicChanged(isOn));
|
||||
});
|
||||
|
||||
ringToggle.setOnCheckedChangeListener((v, isOn) -> {
|
||||
runIfNonNull(controlsListener, listener -> listener.onRingGroupChanged(isOn, ringToggle.isActivated()));
|
||||
});
|
||||
|
||||
cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
|
||||
|
||||
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
|
||||
@@ -232,7 +270,6 @@ public class WebRtcCallView extends FrameLayout {
|
||||
controlsListener.onStartCall(videoToggle.isChecked());
|
||||
}
|
||||
});
|
||||
cancelStartCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCancelStartCall));
|
||||
|
||||
ColorMatrix greyScaleMatrix = new ColorMatrix();
|
||||
greyScaleMatrix.setSaturation(0);
|
||||
@@ -254,6 +291,10 @@ public class WebRtcCallView extends FrameLayout {
|
||||
rotatableControls.add(cameraDirectionToggle);
|
||||
rotatableControls.add(decline);
|
||||
rotatableControls.add(smallLocalRender.findViewById(R.id.call_participant_mic_muted));
|
||||
rotatableControls.add(ringToggle);
|
||||
|
||||
smallHeaderConstraints = new ConstraintSet();
|
||||
smallHeaderConstraints.clone(getContext(), R.layout.webrtc_call_view_header_small);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -267,7 +308,6 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
@Override
|
||||
protected boolean fitSystemWindows(Rect insets) {
|
||||
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
|
||||
Guideline navigationBarGuideline = findViewById(R.id.call_screen_navigation_bar_guideline);
|
||||
|
||||
statusBarGuideline.setGuidelineBegin(insets.top);
|
||||
@@ -279,10 +319,18 @@ public class WebRtcCallView extends FrameLayout {
|
||||
@Override
|
||||
public void onWindowSystemUiVisibilityChanged(int visible) {
|
||||
if ((visible & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
|
||||
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
|
||||
if (controls.adjustForFold()) {
|
||||
pictureInPictureGestureHelper.clearVerticalBoundaries();
|
||||
pictureInPictureGestureHelper.setTopVerticalBoundary(toolbar.getTop());
|
||||
} else {
|
||||
pictureInPictureGestureHelper.setTopVerticalBoundary(toolbar.getBottom());
|
||||
pictureInPictureGestureHelper.setBottomVerticalBoundary(videoToggle.getTop());
|
||||
}
|
||||
} else {
|
||||
pictureInPictureGestureHelper.clearVerticalBoundaries();
|
||||
}
|
||||
|
||||
pictureInPictureGestureHelper.adjustPip();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -305,49 +353,67 @@ public class WebRtcCallView extends FrameLayout {
|
||||
micToggle.setChecked(isMicEnabled, false);
|
||||
}
|
||||
|
||||
public void updateCallParticipants(@NonNull CallParticipantsState state, boolean isPortrait) {
|
||||
List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
|
||||
public void updateCallParticipants(@NonNull CallParticipantsViewState callParticipantsViewState) {
|
||||
CallParticipantsState state = callParticipantsViewState.getCallParticipantsState();
|
||||
boolean isPortrait = callParticipantsViewState.isPortrait();
|
||||
boolean isLandscapeEnabled = callParticipantsViewState.isLandscapeEnabled();
|
||||
List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
|
||||
|
||||
if (!state.getGridParticipants().isEmpty()) {
|
||||
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode(), isPortrait));
|
||||
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode(), isPortrait, isLandscapeEnabled, state.isIncomingRing()));
|
||||
}
|
||||
|
||||
if (state.getFocusedParticipant() != CallParticipant.EMPTY && state.getAllRemoteParticipants().size() > 1) {
|
||||
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode(), isPortrait));
|
||||
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode(), isPortrait, isLandscapeEnabled));
|
||||
}
|
||||
|
||||
if ((state.getGroupCallState().isNotIdle() && state.getRemoteDevicesCount().orElse(0) > 0) || state.getGroupCallState().isConnected()) {
|
||||
recipientName.setText(state.getRemoteParticipantsDescription(getContext()));
|
||||
} else if (state.getGroupCallState().isNotIdle()) {
|
||||
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, Recipient.resolved(recipientId).getDisplayName(getContext())));
|
||||
if (state.getGroupCallState().isNotIdle()) {
|
||||
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
status.setText(state.getPreJoinGroupDescription(getContext()));
|
||||
} else if (state.getCallState() == WebRtcViewModel.State.CALL_CONNECTED && state.isInOutgoingRingingMode()) {
|
||||
status.setText(state.getOutgoingRingingGroupDescription(getContext()));
|
||||
} else if (state.getGroupCallState().isRinging()) {
|
||||
status.setText(state.getIncomingRingingGroupDescription(getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.getGroupCallState().isNotIdle() && participantCount != null) {
|
||||
participantCount.setText(state.getParticipantCount()
|
||||
.mapToObj(String::valueOf).orElse("\u2014"));
|
||||
participantCount.setEnabled(state.getParticipantCount().isPresent());
|
||||
if (state.getGroupCallState().isNotIdle()) {
|
||||
String text = state.getParticipantCount()
|
||||
.mapToObj(String::valueOf).orElse("\u2014");
|
||||
boolean enabled = state.getParticipantCount().isPresent();
|
||||
|
||||
foldParticipantCount.setText(text);
|
||||
foldParticipantCount.setEnabled(enabled);
|
||||
}
|
||||
|
||||
pagerAdapter.submitList(pages);
|
||||
recyclerAdapter.submitList(state.getListParticipants());
|
||||
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant(), state.getFocusedParticipant());
|
||||
|
||||
if (state.isLargeVideoGroup() && !state.isInPipMode()) {
|
||||
boolean displaySmallSelfPipInLandscape = !isPortrait && isLandscapeEnabled;
|
||||
|
||||
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant(), state.getFocusedParticipant(), displaySmallSelfPipInLandscape);
|
||||
|
||||
if (state.isLargeVideoGroup() && !state.isInPipMode() && !state.isFolded()) {
|
||||
layoutParticipantsForLargeCount();
|
||||
} else {
|
||||
layoutParticipantsForSmallCount();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
|
||||
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state,
|
||||
@NonNull CallParticipant localCallParticipant,
|
||||
@NonNull CallParticipant focusedParticipant,
|
||||
boolean displaySmallSelfPipInLandscape)
|
||||
{
|
||||
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
|
||||
|
||||
smallLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
|
||||
largeLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
|
||||
|
||||
if (localCallParticipant.getVideoSink().getEglBase() != null) {
|
||||
largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
|
||||
}
|
||||
localCallParticipant.getVideoSink().getLockableEglBase().performWithValidEglBase(eglBase -> {
|
||||
largeLocalRender.init(eglBase);
|
||||
});
|
||||
|
||||
|
||||
videoToggle.setChecked(localCallParticipant.isVideoEnabled(), false);
|
||||
smallLocalRender.setRenderInPip(true);
|
||||
@@ -372,7 +438,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
break;
|
||||
case SMALL_RECTANGLE:
|
||||
smallLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
animatePipToLargeRectangle();
|
||||
animatePipToLargeRectangle(displaySmallSelfPipInLandscape);
|
||||
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
@@ -418,18 +484,13 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
recipientId = recipient.getId();
|
||||
|
||||
largeHeaderAvatar.setRecipient(recipient, false);
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
|
||||
toolbar.inflateMenu(R.menu.group_call);
|
||||
|
||||
View showParticipants = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list).getActionView();
|
||||
showParticipants.setOnClickListener(unused -> showParticipantsList());
|
||||
|
||||
participantCount = showParticipants.findViewById(R.id.show_participants_menu_counter);
|
||||
}
|
||||
} else {
|
||||
recipientName.setText(recipient.getDisplayName(getContext()));
|
||||
foldParticipantCountWrapper.setOnClickListener(unused -> showParticipantsList());
|
||||
}
|
||||
|
||||
recipientName.setText(recipient.getDisplayName(getContext()));
|
||||
}
|
||||
|
||||
public void setStatus(@NonNull String status) {
|
||||
@@ -480,6 +541,23 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
visibleViewSet.clear();
|
||||
|
||||
if (webRtcControls.adjustForFold()) {
|
||||
showParticipantsGuideline.setGuidelineBegin(-1);
|
||||
showParticipantsGuideline.setGuidelineEnd(webRtcControls.getFold());
|
||||
topFoldGuideline.setGuidelineEnd(webRtcControls.getFold());
|
||||
callScreenTopFoldGuideline.setGuidelineEnd(webRtcControls.getFold());
|
||||
} else {
|
||||
showParticipantsGuideline.setGuidelineBegin(((LayoutParams) statusBarGuideline.getLayoutParams()).guideBegin);
|
||||
showParticipantsGuideline.setGuidelineEnd(-1);
|
||||
topFoldGuideline.setGuidelineEnd(0);
|
||||
callScreenTopFoldGuideline.setGuidelineEnd(0);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayGroupMembersButton()) {
|
||||
visibleViewSet.add(foldParticipantCountWrapper);
|
||||
foldParticipantCount.setClickable(webRtcControls.adjustForFold());
|
||||
}
|
||||
|
||||
if (webRtcControls.displayStartCallControls()) {
|
||||
visibleViewSet.add(footerGradient);
|
||||
visibleViewSet.add(startCallControls);
|
||||
@@ -500,12 +578,6 @@ public class WebRtcCallView extends FrameLayout {
|
||||
groupCallFullStub.get().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
MenuItem item = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list);
|
||||
if (item != null) {
|
||||
item.setVisible(webRtcControls.displayGroupMembersButton());
|
||||
item.setEnabled(webRtcControls.displayGroupMembersButton());
|
||||
}
|
||||
|
||||
if (webRtcControls.displayTopViews()) {
|
||||
visibleViewSet.addAll(topViews);
|
||||
}
|
||||
@@ -513,7 +585,9 @@ public class WebRtcCallView extends FrameLayout {
|
||||
if (webRtcControls.displayIncomingCallButtons()) {
|
||||
visibleViewSet.addAll(incomingCallViews);
|
||||
|
||||
status.setText(R.string.WebRtcCallView__signal_voice_call);
|
||||
incomingRingStatus.setVisibility(VISIBLE);
|
||||
incomingRingStatus.setText(R.string.WebRtcCallView__signal_call);
|
||||
|
||||
answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer));
|
||||
}
|
||||
|
||||
@@ -521,12 +595,19 @@ public class WebRtcCallView extends FrameLayout {
|
||||
visibleViewSet.add(answerWithAudio);
|
||||
visibleViewSet.add(answerWithAudioLabel);
|
||||
|
||||
status.setText(R.string.WebRtcCallView__signal_video_call);
|
||||
incomingRingStatus.setVisibility(VISIBLE);
|
||||
incomingRingStatus.setText(R.string.WebRtcCallView__signal_video_call);
|
||||
|
||||
answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer_with_video));
|
||||
}
|
||||
|
||||
if (!webRtcControls.displayIncomingCallButtons() && !webRtcControls.displayAnswerWithAudio()){
|
||||
incomingRingStatus.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayAudioToggle()) {
|
||||
visibleViewSet.add(audioToggle);
|
||||
visibleViewSet.add(audioToggleLabel);
|
||||
|
||||
audioToggle.setControlAvailability(webRtcControls.enableHandsetInAudioToggle(),
|
||||
webRtcControls.enableHeadsetInAudioToggle());
|
||||
@@ -536,19 +617,23 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
if (webRtcControls.displayCameraToggle()) {
|
||||
visibleViewSet.add(cameraDirectionToggle);
|
||||
visibleViewSet.add(cameraDirectionToggleLabel);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayEndCall()) {
|
||||
visibleViewSet.add(hangup);
|
||||
visibleViewSet.add(hangupLabel);
|
||||
visibleViewSet.add(footerGradient);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayMuteAudio()) {
|
||||
visibleViewSet.add(micToggle);
|
||||
visibleViewSet.add(micToggleLabel);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayVideoToggle()) {
|
||||
visibleViewSet.add(videoToggle);
|
||||
visibleViewSet.add(videoToggleLabel);
|
||||
}
|
||||
|
||||
if (webRtcControls.displaySmallOngoingCallButtons()) {
|
||||
@@ -563,6 +648,19 @@ public class WebRtcCallView extends FrameLayout {
|
||||
callParticipantsRecycler.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (webRtcControls.showFullScreenShade()) {
|
||||
fullScreenShade.setVisibility(VISIBLE);
|
||||
visibleViewSet.remove(topGradient);
|
||||
visibleViewSet.remove(footerGradient);
|
||||
} else {
|
||||
fullScreenShade.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayRingToggle()) {
|
||||
visibleViewSet.add(ringToggle);
|
||||
visibleViewSet.add(ringToggleLabel);
|
||||
}
|
||||
|
||||
if (webRtcControls.isFadeOutEnabled()) {
|
||||
if (!controls.isFadeOutEnabled()) {
|
||||
scheduleFadeOut();
|
||||
@@ -575,12 +673,33 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
if (webRtcControls.adjustForFold() && webRtcControls.isFadeOutEnabled() && !controls.adjustForFold()) {
|
||||
scheduleFadeOut();
|
||||
}
|
||||
|
||||
boolean forceUpdate = webRtcControls.adjustForFold() && !controls.adjustForFold();
|
||||
controls = webRtcControls;
|
||||
|
||||
if (!visibleViewSet.equals(lastVisibleSet) || !controls.isFadeOutEnabled()) {
|
||||
fadeInNewUiState(lastVisibleSet, webRtcControls.displaySmallOngoingCallButtons());
|
||||
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop()));
|
||||
if (!controls.isFadeOutEnabled()) {
|
||||
controlsVisible = true;
|
||||
}
|
||||
|
||||
allTimeVisibleViews.addAll(visibleViewSet);
|
||||
|
||||
if (!visibleViewSet.equals(lastVisibleSet) ||
|
||||
!controls.isFadeOutEnabled() ||
|
||||
(webRtcControls.showSmallHeader() && largeHeaderAvatar.getVisibility() == View.VISIBLE) ||
|
||||
forceUpdate)
|
||||
{
|
||||
|
||||
if (controlsListener != null) {
|
||||
controlsListener.showSystemUI();
|
||||
}
|
||||
|
||||
throttledDebouncer.publish(() -> fadeInNewUiState(webRtcControls.displaySmallOngoingCallButtons(), webRtcControls.showSmallHeader()));
|
||||
}
|
||||
|
||||
onWindowSystemUiVisibilityChanged(getWindowSystemUiVisibility());
|
||||
}
|
||||
|
||||
public @NonNull View getVideoTooltipTarget() {
|
||||
@@ -653,8 +772,15 @@ public class WebRtcCallView extends FrameLayout {
|
||||
});
|
||||
}
|
||||
|
||||
private void animatePipToLargeRectangle() {
|
||||
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
|
||||
private void animatePipToLargeRectangle(boolean isLandscape) {
|
||||
final Point dimens;
|
||||
if (isLandscape) {
|
||||
dimens = new Point(ViewUtil.dpToPx(160), ViewUtil.dpToPx(90));
|
||||
} else {
|
||||
dimens = new Point(ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
|
||||
}
|
||||
|
||||
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, dimens.x, dimens.y);
|
||||
animation.setDuration(PIP_RESIZE_DURATION);
|
||||
animation.setAnimationListener(new SimpleAnimationListener() {
|
||||
@Override
|
||||
@@ -720,7 +846,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return controlsVisible ? margin + CONTROLS_HEIGHT : margin;
|
||||
return (controlsVisible || controls.adjustForFold()) ? margin + CONTROLS_HEIGHT : margin;
|
||||
}
|
||||
|
||||
private void layoutParticipants() {
|
||||
@@ -756,25 +882,34 @@ public class WebRtcCallView extends FrameLayout {
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(parent);
|
||||
|
||||
for (View view : visibleViewSet) {
|
||||
for (View view : controlsToFade()) {
|
||||
constraintSet.setVisibility(view.getId(), visibility);
|
||||
}
|
||||
|
||||
adjustParticipantsRecycler(constraintSet);
|
||||
|
||||
constraintSet.applyTo(parent);
|
||||
|
||||
layoutParticipants();
|
||||
}
|
||||
|
||||
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {
|
||||
private Set<View> controlsToFade() {
|
||||
if (controls.adjustForFold()) {
|
||||
return Sets.intersection(topViews, visibleViewSet);
|
||||
} else {
|
||||
return visibleViewSet;
|
||||
}
|
||||
}
|
||||
|
||||
private void fadeInNewUiState(boolean useSmallMargins, boolean showSmallHeader) {
|
||||
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
|
||||
|
||||
TransitionManager.endTransitions(parent);
|
||||
TransitionManager.beginDelayedTransition(parent, transition);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(parent);
|
||||
|
||||
for (View view : SetUtil.difference(previouslyVisibleViewSet, visibleViewSet)) {
|
||||
for (View view : SetUtil.difference(allTimeVisibleViews, visibleViewSet)) {
|
||||
constraintSet.setVisibility(view.getId(), ConstraintSet.GONE);
|
||||
}
|
||||
|
||||
@@ -789,7 +924,23 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
adjustParticipantsRecycler(constraintSet);
|
||||
|
||||
constraintSet.applyTo(parent);
|
||||
|
||||
if (showSmallHeader) {
|
||||
smallHeaderConstraints.applyTo(toolbar);
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustParticipantsRecycler(@NonNull ConstraintSet constraintSet) {
|
||||
if (controlsVisible || controls.adjustForFold()) {
|
||||
constraintSet.connect(R.id.call_screen_participants_recycler, ConstraintSet.BOTTOM, R.id.call_screen_video_toggle, ConstraintSet.TOP);
|
||||
} else {
|
||||
constraintSet.connect(R.id.call_screen_participants_recycler, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM);
|
||||
}
|
||||
|
||||
constraintSet.setHorizontalBias(R.id.call_screen_participants_recycler, controls.adjustForFold() ? 0.5f : 1f);
|
||||
}
|
||||
|
||||
private void scheduleFadeOut() {
|
||||
@@ -816,6 +967,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle);
|
||||
videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle);
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle);
|
||||
ringToggle.setBackgroundResource(R.drawable.webrtc_call_screen_ring_toggle);
|
||||
}
|
||||
|
||||
private void updateButtonStateForSmallButtons() {
|
||||
@@ -824,6 +976,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle_small);
|
||||
videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle_small);
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
|
||||
ringToggle.setBackgroundResource(R.drawable.webrtc_call_screen_ring_toggle_small);
|
||||
}
|
||||
|
||||
private boolean showParticipantsList() {
|
||||
@@ -837,6 +990,14 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setRingGroup(boolean shouldRingGroup) {
|
||||
ringToggle.setChecked(shouldRingGroup, false);
|
||||
}
|
||||
|
||||
public void enableRingGroup(boolean enabled) {
|
||||
ringToggle.setActivated(enabled);
|
||||
}
|
||||
|
||||
public interface ControlsListener {
|
||||
void onStartCall(boolean isVideoCall);
|
||||
void onCancelStartCall();
|
||||
@@ -854,5 +1015,6 @@ public class WebRtcCallView extends FrameLayout {
|
||||
void onShowParticipantsList();
|
||||
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
|
||||
void onLocalPictureInPictureClicked();
|
||||
void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,25 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
||||
import org.thoughtcrime.securesms.components.sensors.Orientation;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId;
|
||||
@@ -27,6 +31,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
@@ -38,28 +43,38 @@ import java.util.Objects;
|
||||
|
||||
public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
|
||||
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
|
||||
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
|
||||
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
|
||||
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
|
||||
private final MutableLiveData<Collection<RecipientId>> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList());
|
||||
private final LiveData<SafetyNumberChangeEvent> safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new);
|
||||
private final LiveData<Recipient> groupRecipient = LiveDataUtil.filter(Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData), Recipient::isActiveGroup);
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> groupMembers = LiveDataUtil.skip(Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())), 1);
|
||||
private final LiveData<Boolean> shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint);
|
||||
private final LiveData<Orientation> orientation;
|
||||
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
|
||||
private final MutableLiveData<WebRtcControls.FoldableState> foldableState = new MutableLiveData<>(WebRtcControls.FoldableState.flat());
|
||||
private final LiveData<WebRtcControls> controlsWithFoldableState = LiveDataUtil.combineLatest(foldableState, webRtcControls, this::updateControlsFoldableState);
|
||||
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, controlsWithFoldableState, this::getRealWebRtcControls);
|
||||
private final SingleLiveEvent<Event> events = new SingleLiveEvent<>();
|
||||
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
private final DefaultValueLiveData<CallParticipantsState> participantsState = new DefaultValueLiveData<>(CallParticipantsState.STARTING_STATE);
|
||||
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
|
||||
private final MutableLiveData<Collection<RecipientId>> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList());
|
||||
private final LiveData<SafetyNumberChangeEvent> safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new);
|
||||
private final LiveData<Recipient> groupRecipient = LiveDataUtil.filter(Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData), Recipient::isActiveGroup);
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> groupMembers = Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers()));
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> groupMembersChanged = LiveDataUtil.skip(groupMembers, 1);
|
||||
private final LiveData<Integer> groupMemberCount = Transformations.map(groupMembers, List::size);
|
||||
private final LiveData<Boolean> shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint);
|
||||
private final LiveData<Orientation> orientation;
|
||||
private final MutableLiveData<Boolean> isLandscapeEnabled = new MutableLiveData<>();
|
||||
private final LiveData<Integer> controlsRotation;
|
||||
private final Observer<List<GroupMemberEntry.FullMember>> groupMemberStateUpdater = m -> participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), m));
|
||||
|
||||
private final Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable elapsedTimeRunnable = this::handleTick;
|
||||
private final Runnable stopOutgoingRingingMode = this::stopOutgoingRingingMode;
|
||||
|
||||
private boolean canDisplayTooltipIfNeeded = true;
|
||||
private boolean hasEnabledLocalVideo = false;
|
||||
private boolean wasInOutgoingRingingMode = false;
|
||||
private long callConnectedTime = -1;
|
||||
private Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean answerWithVideoAvailable = false;
|
||||
private Runnable elapsedTimeRunnable = this::handleTick;
|
||||
private boolean canEnterPipMode = false;
|
||||
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
|
||||
private boolean callStarting = false;
|
||||
@@ -69,13 +84,26 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication());
|
||||
|
||||
private WebRtcCallViewModel(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) {
|
||||
orientation = deviceOrientationMonitor.getOrientation();
|
||||
orientation = deviceOrientationMonitor.getOrientation();
|
||||
controlsRotation = LiveDataUtil.combineLatest(Transformations.distinctUntilChanged(isLandscapeEnabled),
|
||||
Transformations.distinctUntilChanged(orientation),
|
||||
this::resolveRotation);
|
||||
|
||||
groupMembers.observeForever(groupMemberStateUpdater);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getControlsRotation() {
|
||||
return controlsRotation;
|
||||
}
|
||||
|
||||
public LiveData<Orientation> getOrientation() {
|
||||
return Transformations.distinctUntilChanged(orientation);
|
||||
}
|
||||
|
||||
public LiveData<Pair<Orientation, Boolean>> getOrientationAndLandscapeEnabled() {
|
||||
return LiveDataUtil.combineLatest(orientation, isLandscapeEnabled, Pair::new);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getMicrophoneEnabled() {
|
||||
return Transformations.distinctUntilChanged(microphoneEnabled);
|
||||
}
|
||||
@@ -92,6 +120,12 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
liveRecipient.setValue(recipient.live());
|
||||
}
|
||||
|
||||
public void setFoldableState(@NonNull WebRtcControls.FoldableState foldableState) {
|
||||
this.foldableState.postValue(foldableState);
|
||||
|
||||
ThreadUtil.runOnMain(() -> participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), foldableState)));
|
||||
}
|
||||
|
||||
public LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
@@ -112,8 +146,12 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
return safetyNumberChangeEvent;
|
||||
}
|
||||
|
||||
public LiveData<List<GroupMemberEntry.FullMember>> getGroupMembers() {
|
||||
return groupMembers;
|
||||
public LiveData<List<GroupMemberEntry.FullMember>> getGroupMembersChanged() {
|
||||
return groupMembersChanged;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getGroupMemberCount() {
|
||||
return groupMemberCount;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> shouldShowSpeakerHint() {
|
||||
@@ -136,10 +174,13 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
public void setIsInPipMode(boolean isInPipMode) {
|
||||
this.isInPipMode.setValue(isInPipMode);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), isInPipMode));
|
||||
}
|
||||
|
||||
public void setIsLandscapeEnabled(boolean isLandscapeEnabled) {
|
||||
this.isLandscapeEnabled.postValue(isLandscapeEnabled);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setIsViewingFocusedParticipant(@NonNull CallParticipantsState.SelectedPage page) {
|
||||
if (page == CallParticipantsState.SelectedPage.FOCUSED) {
|
||||
@@ -147,11 +188,11 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
if (state != null &&
|
||||
showScreenShareTip &&
|
||||
if (showScreenShareTip &&
|
||||
state.getFocusedParticipant().isScreenSharing() &&
|
||||
state.isViewingFocusedParticipant() &&
|
||||
page == CallParticipantsState.SelectedPage.GRID) {
|
||||
page == CallParticipantsState.SelectedPage.GRID)
|
||||
{
|
||||
showScreenShareTip = false;
|
||||
events.setValue(new Event.ShowSwipeToSpeakerHint());
|
||||
}
|
||||
@@ -184,15 +225,14 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
|
||||
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
if (state != null) {
|
||||
boolean wasScreenSharing = state.getFocusedParticipant().isScreenSharing();
|
||||
CallParticipantsState newState = CallParticipantsState.update(state, webRtcViewModel, enableVideo);
|
||||
participantsState.setValue(newState);
|
||||
if (switchOnFirstScreenShare && !wasScreenSharing && newState.getFocusedParticipant().isScreenSharing()) {
|
||||
switchOnFirstScreenShare = false;
|
||||
events.setValue(new Event.SwitchToSpeaker());
|
||||
}
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
boolean wasScreenSharing = state.getFocusedParticipant().isScreenSharing();
|
||||
CallParticipantsState newState = CallParticipantsState.update(state, webRtcViewModel, enableVideo);
|
||||
|
||||
participantsState.setValue(newState);
|
||||
if (switchOnFirstScreenShare && !wasScreenSharing && newState.getFocusedParticipant().isScreenSharing()) {
|
||||
switchOnFirstScreenShare = false;
|
||||
events.setValue(new Event.SwitchToSpeaker());
|
||||
}
|
||||
|
||||
if (webRtcViewModel.getGroupState().isConnected()) {
|
||||
@@ -218,12 +258,20 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
webRtcViewModel.getRemoteDevicesCount().orElse(0),
|
||||
webRtcViewModel.getParticipantLimit());
|
||||
|
||||
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
|
||||
callConnectedTime = webRtcViewModel.getCallConnectedTime();
|
||||
startTimer();
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED || webRtcViewModel.getGroupState().isNotIdleOrConnected()) {
|
||||
if (newState.isInOutgoingRingingMode()) {
|
||||
cancelTimer();
|
||||
callConnectedTime = -1;
|
||||
if (!wasInOutgoingRingingMode) {
|
||||
elapsedTimeHandler.postDelayed(stopOutgoingRingingMode, CallParticipantsState.MAX_OUTGOING_GROUP_RING_DURATION);
|
||||
}
|
||||
wasInOutgoingRingingMode = true;
|
||||
} else {
|
||||
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
|
||||
callConnectedTime = wasInOutgoingRingingMode ? System.currentTimeMillis() : webRtcViewModel.getCallConnectedTime();
|
||||
startTimer();
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED || webRtcViewModel.getGroupState().isNotIdleOrConnected()) {
|
||||
cancelTimer();
|
||||
callConnectedTime = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (localParticipant.getCameraState().isEnabled()) {
|
||||
@@ -239,6 +287,23 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private int resolveRotation(boolean isLandscapeEnabled, @NonNull Orientation orientation) {
|
||||
if (isLandscapeEnabled) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (orientation) {
|
||||
case LANDSCAPE_LEFT_EDGE:
|
||||
return 90;
|
||||
case LANDSCAPE_RIGHT_EDGE:
|
||||
return -90;
|
||||
case PORTRAIT_BOTTOM_EDGE:
|
||||
return 0;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsPlaceholders(@NonNull List<CallParticipant> callParticipants) {
|
||||
return Stream.of(callParticipants).anyMatch(p -> p.getCallParticipantId().getDemuxId() == CallParticipantId.DEFAULT_ID);
|
||||
}
|
||||
@@ -314,7 +379,12 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
callState,
|
||||
groupCallState,
|
||||
audioOutput,
|
||||
participantLimit));
|
||||
participantLimit,
|
||||
WebRtcControls.FoldableState.flat()));
|
||||
}
|
||||
|
||||
private @NonNull WebRtcControls updateControlsFoldableState(@NonNull WebRtcControls.FoldableState foldableState, @NonNull WebRtcControls controls) {
|
||||
return controls.withFoldableState(foldableState);
|
||||
}
|
||||
|
||||
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
|
||||
@@ -322,18 +392,26 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
private boolean shouldShowSpeakerHint(@NonNull CallParticipantsState state) {
|
||||
return !state.isInPipMode() &&
|
||||
return !state.isInPipMode() &&
|
||||
state.getRemoteDevicesCount().orElse(0) > 1 &&
|
||||
state.getGroupCallState().isConnected() &&
|
||||
state.getGroupCallState().isConnected() &&
|
||||
!SignalStore.tooltips().hasSeenGroupCallSpeakerView();
|
||||
}
|
||||
|
||||
private void startTimer() {
|
||||
cancelTimer();
|
||||
elapsedTimeHandler.removeCallbacks(stopOutgoingRingingMode);
|
||||
|
||||
elapsedTimeHandler.post(elapsedTimeRunnable);
|
||||
}
|
||||
|
||||
private void stopOutgoingRingingMode() {
|
||||
if (callConnectedTime == -1) {
|
||||
callConnectedTime = System.currentTimeMillis();
|
||||
startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTick() {
|
||||
if (callConnectedTime == -1) {
|
||||
return;
|
||||
@@ -354,6 +432,7 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
cancelTimer();
|
||||
groupMembers.removeObserver(groupMemberStateUpdater);
|
||||
}
|
||||
|
||||
public void startCall(boolean isVideoCall) {
|
||||
@@ -362,7 +441,7 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
if (recipient.isGroup()) {
|
||||
repository.getIdentityRecords(recipient, identityRecords -> {
|
||||
if (identityRecords.isUntrusted(false) || identityRecords.isUnverified(false)) {
|
||||
List<IdentityDatabase.IdentityRecord> records = identityRecords.getUnverifiedRecords();
|
||||
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
|
||||
records.addAll(identityRecords.getUntrustedRecords());
|
||||
events.postValue(new Event.ShowGroupCallSafetyNumberChange(records));
|
||||
} else {
|
||||
@@ -397,13 +476,13 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public static class ShowGroupCallSafetyNumberChange extends Event {
|
||||
private final List<IdentityDatabase.IdentityRecord> identityRecords;
|
||||
private final List<IdentityRecord> identityRecords;
|
||||
|
||||
public ShowGroupCallSafetyNumberChange(@NonNull List<IdentityDatabase.IdentityRecord> identityRecords) {
|
||||
public ShowGroupCallSafetyNumberChange(@NonNull List<IdentityRecord> identityRecords) {
|
||||
this.identityRecords = identityRecords;
|
||||
}
|
||||
|
||||
public @NonNull List<IdentityDatabase.IdentityRecord> getIdentityRecords() {
|
||||
public @NonNull List<IdentityRecord> getIdentityRecords() {
|
||||
return identityRecords;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,16 @@ import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
public final class WebRtcControls {
|
||||
|
||||
public static final WebRtcControls NONE = new WebRtcControls();
|
||||
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null);
|
||||
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null, FoldableState.flat());
|
||||
|
||||
private final boolean isRemoteVideoEnabled;
|
||||
private final boolean isLocalVideoEnabled;
|
||||
@@ -23,9 +25,10 @@ public final class WebRtcControls {
|
||||
private final GroupCallState groupCallState;
|
||||
private final WebRtcAudioOutput audioOutput;
|
||||
private final Long participantLimit;
|
||||
private final FoldableState foldableState;
|
||||
|
||||
private WebRtcControls() {
|
||||
this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null);
|
||||
this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null, FoldableState.flat());
|
||||
}
|
||||
|
||||
WebRtcControls(boolean isLocalVideoEnabled,
|
||||
@@ -37,7 +40,8 @@ public final class WebRtcControls {
|
||||
@NonNull CallState callState,
|
||||
@NonNull GroupCallState groupCallState,
|
||||
@NonNull WebRtcAudioOutput audioOutput,
|
||||
@Nullable Long participantLimit)
|
||||
@Nullable Long participantLimit,
|
||||
@NonNull FoldableState foldableState)
|
||||
{
|
||||
this.isLocalVideoEnabled = isLocalVideoEnabled;
|
||||
this.isRemoteVideoEnabled = isRemoteVideoEnabled;
|
||||
@@ -49,6 +53,21 @@ public final class WebRtcControls {
|
||||
this.groupCallState = groupCallState;
|
||||
this.audioOutput = audioOutput;
|
||||
this.participantLimit = participantLimit;
|
||||
this.foldableState = foldableState;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcControls withFoldableState(FoldableState foldableState) {
|
||||
return new WebRtcControls(isLocalVideoEnabled,
|
||||
isRemoteVideoEnabled,
|
||||
isMoreThanOneCameraAvailable,
|
||||
isBluetoothAvailable,
|
||||
isInPipMode,
|
||||
hasAtLeastOneRemote,
|
||||
callState,
|
||||
groupCallState,
|
||||
audioOutput,
|
||||
participantLimit,
|
||||
foldableState);
|
||||
}
|
||||
|
||||
boolean displayErrorControls() {
|
||||
@@ -59,6 +78,14 @@ public final class WebRtcControls {
|
||||
return isPreJoin();
|
||||
}
|
||||
|
||||
boolean adjustForFold() {
|
||||
return foldableState.isFolded();
|
||||
}
|
||||
|
||||
@Px int getFold() {
|
||||
return foldableState.getFoldPoint();
|
||||
}
|
||||
|
||||
@StringRes int getStartCallButtonText() {
|
||||
if (isGroupCall()) {
|
||||
if (groupCallState == GroupCallState.FULL) {
|
||||
@@ -86,7 +113,7 @@ public final class WebRtcControls {
|
||||
}
|
||||
|
||||
boolean displayGroupMembersButton() {
|
||||
return groupCallState.isAtLeast(GroupCallState.CONNECTING);
|
||||
return (groupCallState.isAtLeast(GroupCallState.CONNECTING) && hasAtLeastOneRemote) || groupCallState.isAtLeast(GroupCallState.FULL);
|
||||
}
|
||||
|
||||
boolean displayEndCall() {
|
||||
@@ -149,6 +176,18 @@ public final class WebRtcControls {
|
||||
return audioOutput;
|
||||
}
|
||||
|
||||
boolean showSmallHeader() {
|
||||
return isAtLeastOutgoing();
|
||||
}
|
||||
|
||||
boolean showFullScreenShade() {
|
||||
return isPreJoin() || isIncoming();
|
||||
}
|
||||
|
||||
boolean displayRingToggle() {
|
||||
return FeatureFlags.groupCallRinging() && isPreJoin() && isGroupCall() && !hasAtLeastOneRemote;
|
||||
}
|
||||
|
||||
private boolean isError() {
|
||||
return callState == CallState.ERROR;
|
||||
}
|
||||
@@ -199,4 +238,35 @@ public final class WebRtcControls {
|
||||
return compareTo(other) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class FoldableState {
|
||||
|
||||
private static final int NOT_SET = -1;
|
||||
|
||||
private final int foldPoint;
|
||||
|
||||
public FoldableState(int foldPoint) {
|
||||
this.foldPoint = foldPoint;
|
||||
}
|
||||
|
||||
public boolean isFolded() {
|
||||
return foldPoint != NOT_SET;
|
||||
}
|
||||
|
||||
public boolean isFlat() {
|
||||
return foldPoint == NOT_SET;
|
||||
}
|
||||
|
||||
public int getFoldPoint() {
|
||||
return foldPoint;
|
||||
}
|
||||
|
||||
public static @NonNull FoldableState folded(int foldPoint) {
|
||||
return new FoldableState(foldPoint);
|
||||
}
|
||||
|
||||
public static @NonNull FoldableState flat() {
|
||||
return new FoldableState(NOT_SET);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -25,7 +26,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public class ContactSelectionListItem extends LinearLayout implements RecipientForeverObserver {
|
||||
public class ContactSelectionListItem extends ConstraintLayout implements RecipientForeverObserver {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ContactSelectionListItem.class);
|
||||
@@ -216,7 +217,14 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
|
||||
if (this.recipient != null && this.recipient.getId().equals(recipient.getId())) {
|
||||
contactName = recipient.getDisplayName(getContext());
|
||||
contactAbout = recipient.getCombinedAboutAndEmoji();
|
||||
contactNumber = PhoneNumberFormatter.prettyPrint(recipient.getE164().or(""));
|
||||
|
||||
if (recipient.isGroup() && recipient.getGroupId().isPresent()) {
|
||||
contactNumber = recipient.getGroupId().get().toString();
|
||||
} else if (recipient.hasE164()) {
|
||||
contactNumber = PhoneNumberFormatter.prettyPrint(recipient.getE164().or(""));
|
||||
} else {
|
||||
contactNumber = recipient.getEmail().or("");
|
||||
}
|
||||
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient, false);
|
||||
setText(recipient, contactType, contactName, contactNumber, contactLabel, contactAbout);
|
||||
|
||||
@@ -26,12 +26,9 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
|
||||
@@ -48,15 +45,16 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||
private static final String TAG = Log.tag(ContactsCursorLoader.class);
|
||||
|
||||
public static final class DisplayMode {
|
||||
public static final int FLAG_PUSH = 1;
|
||||
public static final int FLAG_SMS = 1 << 1;
|
||||
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
|
||||
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
|
||||
public static final int FLAG_SELF = 1 << 4;
|
||||
public static final int FLAG_BLOCK = 1 << 5;
|
||||
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
|
||||
public static final int FLAG_HIDE_NEW = 1 << 6;
|
||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
||||
public static final int FLAG_PUSH = 1;
|
||||
public static final int FLAG_SMS = 1 << 1;
|
||||
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
|
||||
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
|
||||
public static final int FLAG_SELF = 1 << 4;
|
||||
public static final int FLAG_BLOCK = 1 << 5;
|
||||
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
|
||||
public static final int FLAG_HIDE_NEW = 1 << 6;
|
||||
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
|
||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
||||
}
|
||||
|
||||
private static final int RECENT_CONVERSATION_MAX = 25;
|
||||
@@ -115,7 +113,9 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||
Cursor recentConversations = getRecentConversationsCursor();
|
||||
|
||||
if (recentConversations.getCount() > 0) {
|
||||
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
|
||||
if (!hideRecentsHeader(mode)) {
|
||||
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
|
||||
}
|
||||
cursorList.add(recentConversations);
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,9 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||
Cursor groups = getRecentConversationsCursor(true);
|
||||
|
||||
if (groups.getCount() > 0) {
|
||||
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
|
||||
if (!hideRecentsHeader(mode)) {
|
||||
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
|
||||
}
|
||||
cursorList.add(groups);
|
||||
}
|
||||
}
|
||||
@@ -279,6 +281,10 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||
return flagSet(mode, DisplayMode.FLAG_HIDE_NEW);
|
||||
}
|
||||
|
||||
private static boolean hideRecentsHeader(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_HIDE_RECENT_HEADER);
|
||||
}
|
||||
|
||||
private static boolean flagSet(int mode, int flag) {
|
||||
return (mode & flag) > 0;
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ public class DirectoryHelper {
|
||||
recipient = Recipient.resolved(recipientDatabase.getByUuid(uuid).get());
|
||||
}
|
||||
} else {
|
||||
recipientDatabase.markRegistered(recipient.getId());
|
||||
Log.w(TAG, "Registered number set had a null UUID!");
|
||||
}
|
||||
} else if (recipient.hasUuid() && recipient.isRegistered() && hasCommunicatedWith(context, recipient)) {
|
||||
if (isUuidRegistered(context, recipient)) {
|
||||
@@ -469,8 +469,8 @@ public class DirectoryHelper {
|
||||
|
||||
for (RecipientId newUser: newUsers) {
|
||||
Recipient recipient = Recipient.resolved(newUser);
|
||||
if (!SessionUtil.hasSession(context, recipient.getId()) &&
|
||||
!recipient.isSelf() &&
|
||||
if (!SessionUtil.hasSession(recipient.getId()) &&
|
||||
!recipient.isSelf() &&
|
||||
recipient.hasAUserSetDisplayName(context))
|
||||
{
|
||||
IncomingJoinedMessage message = new IncomingJoinedMessage(recipient.getId());
|
||||
@@ -543,8 +543,9 @@ public class DirectoryHelper {
|
||||
}
|
||||
|
||||
private static boolean hasCommunicatedWith(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
return DatabaseFactory.getThreadDatabase(context).hasThread(recipient.getId()) ||
|
||||
DatabaseFactory.getSessionDatabase(context).hasSessionFor(recipient.getId());
|
||||
return DatabaseFactory.getThreadDatabase(context).hasThread(recipient.getId()) ||
|
||||
(recipient.hasUuid() && DatabaseFactory.getSessionDatabase(context).hasSessionFor(recipient.requireUuid().toString())) ||
|
||||
(recipient.hasE164() && DatabaseFactory.getSessionDatabase(context).hasSessionFor(recipient.requireE164()));
|
||||
}
|
||||
|
||||
static class DirectoryResult {
|
||||
|
||||
@@ -25,7 +25,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
@@ -44,6 +43,7 @@ import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
@@ -159,7 +159,7 @@ import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions;
|
||||
@@ -205,8 +205,8 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
|
||||
@@ -276,6 +276,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SmsUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -296,6 +297,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -394,6 +396,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private InputPanel inputPanel;
|
||||
private View panelParent;
|
||||
private View noLongerMemberBanner;
|
||||
private Stub<TextView> cannotSendInAnnouncementGroupBanner;
|
||||
private View requestingMemberBanner;
|
||||
private View cancelJoinRequest;
|
||||
private Stub<View> mentionsSuggestions;
|
||||
@@ -421,8 +424,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private long threadId;
|
||||
private int distributionType;
|
||||
private boolean isSecureText;
|
||||
private boolean isDefaultSms;
|
||||
private int reactWithAnyEmojiStartPage = -1;
|
||||
private boolean isDefaultSms = true;
|
||||
private boolean isMmsEnabled = true;
|
||||
private boolean isSecurityInitialized = false;
|
||||
private boolean isSearchRequested = false;
|
||||
@@ -448,7 +451,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
isDefaultSms = Util.isDefaultSmsProvider(this);
|
||||
voiceNoteMediaController = new VoiceNoteMediaController(this);
|
||||
voiceRecorderWakeLock = new VoiceRecorderWakeLock(this);
|
||||
|
||||
@@ -738,7 +741,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
initializeSecurity(isSecureText, isDefaultSms);
|
||||
break;
|
||||
case MEDIA_SENDER:
|
||||
MediaSendActivityResult result = data.getParcelableExtra(MediaSendActivity.EXTRA_RESULT);
|
||||
MediaSendActivityResult result = MediaSendActivityResult.fromData(data);
|
||||
|
||||
if (!Objects.equals(result.getRecipientId(), recipient.getId())) {
|
||||
Log.w(TAG, "Result's recipientId did not match ours! Result: " + result.getRecipientId() + ", Activity: " + recipient.getId());
|
||||
@@ -753,7 +756,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
boolean initiating = threadId == -1;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
@@ -786,7 +789,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
result.isViewOnce(),
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true).addListener(new AssertedSuccessListener<Void>() {
|
||||
true,
|
||||
null).addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
@@ -888,7 +892,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
if (isSecureText) {
|
||||
if (recipient.get().getExpireMessages() > 0) {
|
||||
if (recipient.get().getExpiresInSeconds() > 0) {
|
||||
if (!isInActiveGroup) {
|
||||
inflater.inflate(R.menu.conversation_expiring_on, menu);
|
||||
}
|
||||
@@ -1016,7 +1020,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
searchViewModel.onSearchOpened();
|
||||
searchNav.setVisibility(View.VISIBLE);
|
||||
searchNav.setData(0, 0);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
inputPanel.setHideForSearch(true);
|
||||
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
if (!menu.getItem(i).equals(searchViewItem)) {
|
||||
@@ -1032,7 +1036,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
isSearchRequested = false;
|
||||
searchViewModel.onSearchClosed();
|
||||
searchNav.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.VISIBLE);
|
||||
inputPanel.setHideForSearch(false);
|
||||
fragment.onSearchQueryUpdated(null);
|
||||
setBlockedUserState(recipient.get(), isSecureText, isDefaultSms);
|
||||
invalidateOptionsMenu();
|
||||
@@ -1140,7 +1144,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
@Override
|
||||
public void onAttachmentMediaClicked(@NonNull Media media) {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
startActivityForResult(MediaSelectionActivity.editor(ConversationActivity.this, sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
|
||||
container.hideCurrentInput(composeText);
|
||||
}
|
||||
|
||||
@@ -1148,7 +1152,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
public void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button) {
|
||||
switch (button) {
|
||||
case GALLERY:
|
||||
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport(), inputPanel.getQuote().isPresent());
|
||||
break;
|
||||
case FILE:
|
||||
AttachmentManager.selectDocument(this, PICK_DOCUMENT);
|
||||
@@ -1278,7 +1282,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
new AsyncTask<OutgoingEndSessionMessage, Void, Long>() {
|
||||
@Override
|
||||
protected Long doInBackground(OutgoingEndSessionMessage... messages) {
|
||||
return MessageSender.send(context, messages[0], threadId, false, null);
|
||||
return MessageSender.send(context, messages[0], threadId, false, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1426,7 +1430,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void handleVideo(final Recipient recipient) {
|
||||
if (recipient == null) return;
|
||||
|
||||
CommunicationActions.startVideoCall(this, recipient);
|
||||
if (recipient.isPushV2Group() && groupCallViewModel.hasActiveGroupCall().getValue() == Boolean.FALSE && groupViewModel.isNonAdminInAnnouncementGroup()) {
|
||||
new MaterialAlertDialogBuilder(this).setTitle(R.string.ConversationActivity_cant_start_group_call)
|
||||
.setMessage(R.string.ConversationActivity_only_admins_of_this_group_can_start_a_call)
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss())
|
||||
.show();
|
||||
} else {
|
||||
CommunicationActions.startVideoCall(this, recipient);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisplayGroupRecipients() {
|
||||
@@ -1517,7 +1528,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage();
|
||||
sendMessage(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1596,7 +1607,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (!Util.isEmpty(mediaList)) {
|
||||
Log.d(TAG, "Handling shared Media.");
|
||||
Intent sendIntent = MediaSendActivity.buildEditorIntent(this, mediaList, recipient.get(), draftText, sendButton.getSelectedTransport());
|
||||
Intent sendIntent = MediaSelectionActivity.editor(this, sendButton.getSelectedTransport(), mediaList, recipient.getId(), draftText);
|
||||
startActivityForResult(sendIntent, MEDIA_SENDER);
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
@@ -1623,17 +1634,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private void initializeEnabledCheck() {
|
||||
groupViewModel.getSelfMemberLevel().observe(this, selfMemberShip -> {
|
||||
groupViewModel.getSelfMemberLevel().observe(this, selfMembership -> {
|
||||
boolean canSendMessages;
|
||||
boolean leftGroup;
|
||||
boolean canCancelRequest;
|
||||
|
||||
if (selfMemberShip == null) {
|
||||
if (selfMembership == null) {
|
||||
leftGroup = false;
|
||||
canSendMessages = true;
|
||||
canCancelRequest = false;
|
||||
if (cannotSendInAnnouncementGroupBanner.resolved()) {
|
||||
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
switch (selfMemberShip) {
|
||||
switch (selfMembership.getMemberLevel()) {
|
||||
case NOT_A_MEMBER:
|
||||
leftGroup = true;
|
||||
canSendMessages = false;
|
||||
@@ -1658,10 +1672,22 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (!leftGroup && !canCancelRequest && selfMembership.isAnnouncementGroup() && selfMembership.getMemberLevel() != GroupDatabase.MemberLevel.ADMINISTRATOR) {
|
||||
canSendMessages = false;
|
||||
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.VISIBLE);
|
||||
cannotSendInAnnouncementGroupBanner.get().setMovementMethod(LinkMovementMethod.getInstance());
|
||||
cannotSendInAnnouncementGroupBanner.get().setText(SpanUtil.clickSubstring(this, R.string.ConversationActivity_only_s_can_send_messages, R.string.ConversationActivity_admins, v -> {
|
||||
ShowAdminsBottomSheetDialog.show(getSupportFragmentManager(), getRecipient().requireGroupId().requireV2());
|
||||
}));
|
||||
} else if (cannotSendInAnnouncementGroupBanner.resolved()) {
|
||||
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
noLongerMemberBanner.setVisibility(leftGroup ? View.VISIBLE : View.GONE);
|
||||
requestingMemberBanner.setVisibility(canCancelRequest ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (canCancelRequest) {
|
||||
cancelJoinRequest.setOnClickListener(v -> ConversationGroupViewModel.onCancelJoinRequest(getRecipient(), new AsynchronousCallback.MainThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
@@ -1677,7 +1703,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}.toWorkerCallback()));
|
||||
}
|
||||
|
||||
inputPanel.setVisibility(canSendMessages ? View.VISIBLE : View.GONE);
|
||||
inputPanel.setHideForGroupState(!canSendMessages);
|
||||
inputPanel.setEnabled(canSendMessages);
|
||||
sendButton.setEnabled(canSendMessages);
|
||||
attachButton.setEnabled(canSendMessages);
|
||||
@@ -1931,8 +1957,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
new AsyncTask<Recipient, Void, Pair<IdentityRecordList, String>>() {
|
||||
@Override
|
||||
protected @NonNull Pair<IdentityRecordList, String> doInBackground(Recipient... params) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
|
||||
List<Recipient> recipients;
|
||||
List<Recipient> recipients;
|
||||
|
||||
if (params[0].isGroup()) {
|
||||
recipients = DatabaseFactory.getGroupDatabase(ConversationActivity.this)
|
||||
@@ -1942,7 +1967,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
IdentityRecordList identityRecordList = identityDatabase.getIdentities(recipients);
|
||||
IdentityRecordList identityRecordList = ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Loaded %d identities in %d ms", recipients.size(), System.currentTimeMillis() - startTime));
|
||||
|
||||
@@ -2014,10 +2039,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reactionDelegate = new ConversationReactionDelegate(reactionOverlayStub);
|
||||
|
||||
|
||||
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
|
||||
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
|
||||
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
|
||||
joinGroupCallButton = findViewById(R.id.conversation_group_call_join);
|
||||
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
|
||||
cannotSendInAnnouncementGroupBanner = ViewUtil.findStubById(this, R.id.conversation_cannot_send_announcement_stub);
|
||||
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
|
||||
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
|
||||
joinGroupCallButton = findViewById(R.id.conversation_group_call_join);
|
||||
|
||||
container.setIsBubble(isInBubble());
|
||||
container.addOnKeyboardShownListener(this);
|
||||
@@ -2537,7 +2563,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
Media media = new Media(uri, mimeType, 0, width, height, 0, 0, borderless, videoGif, Optional.absent(), Optional.absent(), Optional.absent());
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
startActivityForResult(MediaSelectionActivity.editor(ConversationActivity.this, sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
|
||||
return new SettableFuture<>(false);
|
||||
} else {
|
||||
return attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height);
|
||||
@@ -2559,10 +2585,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
private void sendSharedContact(List<Contact> contacts) {
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
|
||||
sendMediaMessage(recipient.getId(), isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, false);
|
||||
sendMediaMessage(recipient.getId(), isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, false, null);
|
||||
}
|
||||
|
||||
private void selectContactInfo(ContactData contactData) {
|
||||
@@ -2650,7 +2676,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
long threadId = params[0];
|
||||
|
||||
if (drafts.size() > 0) {
|
||||
if (threadId == -1) threadId = threadDatabase.getThreadIdFor(getRecipient(), thisDistributionType);
|
||||
if (threadId == -1) threadId = threadDatabase.getOrCreateThreadIdFor(getRecipient(), thisDistributionType);
|
||||
|
||||
draftDatabase.replaceDrafts(threadId, drafts);
|
||||
threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this),
|
||||
@@ -2680,17 +2706,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
|
||||
if (!isSecureText && isPushGroupConversation()) {
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
inputPanel.setHideForBlockedState(true);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
registerButton.setVisibility(View.VISIBLE);
|
||||
} else if (!isSecureText && !isDefaultSms && recipient.hasSmsAddress()) {
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
inputPanel.setHideForBlockedState(true);
|
||||
makeDefaultSmsButton.setVisibility(View.VISIBLE);
|
||||
registerButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
boolean inactivePushGroup = isPushGroupConversation() && !recipient.isActiveGroup();
|
||||
inputPanel.setVisibility(inactivePushGroup ? View.GONE : View.VISIBLE);
|
||||
inputPanel.setHideForBlockedState(inactivePushGroup);
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
registerButton.setVisibility(View.GONE);
|
||||
@@ -2821,10 +2847,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
attachmentManager.cleanup();
|
||||
|
||||
updateLinkPreviewState();
|
||||
linkPreviewViewModel.onSend();
|
||||
}
|
||||
|
||||
private void sendMessage() {
|
||||
private void sendMessage(@Nullable String metricId) {
|
||||
if (inputPanel.isRecordingInLockedMode()) {
|
||||
inputPanel.releaseRecordingLock();
|
||||
return;
|
||||
@@ -2850,7 +2875,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
TransportOption transport = sendButton.getSelectedTransport();
|
||||
boolean forceSms = (recipient.isForceSmsSelection() || sendButton.isManualSelection()) && transport.isSms();
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
boolean needsSplit = !transport.isSms() && message.length() > transport.calculateCharacters(message).maxPrimaryMessageSize;
|
||||
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
||||
@@ -2868,9 +2893,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
} else if (!forceSms && (identityRecords.isUnverified(true) || identityRecords.isUntrusted(true))) {
|
||||
handleRecentSafetyNumberChange();
|
||||
} else if (isMediaMessage) {
|
||||
sendMediaMessage(forceSms, expiresIn, false, subscriptionId, initiating);
|
||||
sendMediaMessage(forceSms, expiresIn, false, subscriptionId, initiating, metricId);
|
||||
} else {
|
||||
sendTextMessage(forceSms, expiresIn, subscriptionId, initiating);
|
||||
sendTextMessage(forceSms, expiresIn, subscriptionId, initiating, metricId);
|
||||
}
|
||||
} catch (RecipientFormattingException ex) {
|
||||
Toast.makeText(ConversationActivity.this,
|
||||
@@ -2886,7 +2911,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
private void sendMediaMessage(@NonNull MediaSendActivityResult result) {
|
||||
long thread = this.threadId;
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList(), mentions);
|
||||
@@ -2898,10 +2923,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
silentlySetComposeText("");
|
||||
|
||||
long id = fragment.stageOutgoingMessage(message);
|
||||
long id = fragment.stageOutgoingMessage(secureMessage);
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), thread, () -> fragment.releaseOutgoingMessage(id));
|
||||
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), thread, null);
|
||||
|
||||
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
@@ -2910,23 +2935,25 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}, this::sendComplete);
|
||||
}
|
||||
|
||||
private void sendMediaMessage(final boolean forceSms, final long expiresIn, final boolean viewOnce, final int subscriptionId, final boolean initiating)
|
||||
private void sendMediaMessage(final boolean forceSms, final long expiresIn, final boolean viewOnce, final int subscriptionId, final boolean initiating, @Nullable String metricId)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
Log.i(TAG, "Sending media message...");
|
||||
List<LinkPreview> linkPreviews = linkPreviewViewModel.onSend();
|
||||
sendMediaMessage(recipient.getId(),
|
||||
forceSms,
|
||||
getMessage(),
|
||||
attachmentManager.buildSlideDeck(),
|
||||
inputPanel.getQuote().orNull(),
|
||||
Collections.emptyList(),
|
||||
linkPreviewViewModel.getActiveLinkPreviews(),
|
||||
linkPreviews,
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true);
|
||||
true,
|
||||
metricId);
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> sendMediaMessage(@NonNull RecipientId recipientId,
|
||||
@@ -2941,7 +2968,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
final boolean viewOnce,
|
||||
final int subscriptionId,
|
||||
final boolean initiating,
|
||||
final boolean clearComposeBox)
|
||||
final boolean clearComposeBox,
|
||||
final @Nullable String metricId)
|
||||
{
|
||||
if (!isDefaultSms && (!isSecureText || forceSms) && recipient.get().hasSmsAddress()) {
|
||||
showDefaultSmsPrompt();
|
||||
@@ -2988,7 +3016,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
final long id = fragment.stageOutgoingMessage(outgoingMessage);
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
return MessageSender.send(context, outgoingMessage, thread, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
return MessageSender.send(context, outgoingMessage, thread, forceSms, metricId, null);
|
||||
}, result -> {
|
||||
sendComplete(result);
|
||||
future.set(null);
|
||||
@@ -3000,7 +3028,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return future;
|
||||
}
|
||||
|
||||
private void sendTextMessage(final boolean forceSms, final long expiresIn, final int subscriptionId, final boolean initiating)
|
||||
private void sendTextMessage(final boolean forceSms, final long expiresIn, final int subscriptionId, final boolean initiating, final @Nullable String metricId)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
if (!isDefaultSms && (!isSecureText || forceSms) && recipient.get().hasSmsAddress()) {
|
||||
@@ -3027,12 +3055,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
.ifNecessary(!sendPush)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_sms_permission_in_order_to_send_an_sms))
|
||||
.onAllGranted(() -> {
|
||||
silentlySetComposeText("");
|
||||
final long id = fragment.stageOutgoingMessage(message);
|
||||
|
||||
final long id = new SecureRandom().nextLong();
|
||||
SimpleTask.run(() -> {
|
||||
return MessageSender.send(context, message, thread, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
return MessageSender.send(context, message, thread, forceSms, metricId, null);
|
||||
}, this::sendComplete);
|
||||
|
||||
silentlySetComposeText("");
|
||||
fragment.stageOutgoingMessage(message, id);
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
@@ -3240,7 +3269,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
|
||||
boolean initiating = threadId == -1;
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, uri, size, MediaUtil.AUDIO_AAC, true);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
@@ -3257,7 +3286,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
false,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true);
|
||||
true,
|
||||
null);
|
||||
|
||||
sendResult.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
@@ -3279,12 +3309,12 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull String contentType, @NonNull Uri uri, long size, boolean clearCompose) {
|
||||
if (sendButton.getSelectedTransport().isSms()) {
|
||||
Media media = new Media(uri, contentType, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent());
|
||||
Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
Intent intent = MediaSelectionActivity.editor(this, sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed());
|
||||
startActivityForResult(intent, MEDIA_SENDER);
|
||||
return;
|
||||
}
|
||||
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
boolean initiating = threadId == -1;
|
||||
TransportOption transport = sendButton.getSelectedTransport();
|
||||
@@ -3293,7 +3323,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
slideDeck.addSlide(stickerSlide);
|
||||
|
||||
sendMediaMessage(recipient.getId(), transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
|
||||
sendMediaMessage(recipient.getId(), transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose, null);
|
||||
}
|
||||
|
||||
private void silentlySetComposeText(String text) {
|
||||
@@ -3418,7 +3448,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||
.onAllGranted(() -> {
|
||||
composeText.clearFocus();
|
||||
startActivityForResult(MediaSendActivity.buildCameraIntent(ConversationActivity.this, recipient.get(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
startActivityForResult(MediaSelectionActivity.camera(ConversationActivity.this, sendButton.getSelectedTransport(), recipient.getId(), inputPanel.getQuote().isPresent()), MEDIA_SENDER);
|
||||
overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary);
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
|
||||
@@ -3429,7 +3459,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sendMessage();
|
||||
String metricId = recipient.get().isGroup() ? SignalLocalMetrics.GroupMessageSend.start()
|
||||
: SignalLocalMetrics.IndividualMessageSend.start();
|
||||
sendMessage(metricId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3638,13 +3670,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void handleReaction(@NonNull MaskView.MaskTarget maskTarget,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
|
||||
{
|
||||
reactionDelegate.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionDelegate.setOnHideListener(onHideListener);
|
||||
reactionDelegate.show(this, maskTarget, recipient.get(), messageRecord, inputAreaHeight());
|
||||
reactionDelegate.show(this, maskTarget, recipient.get(), conversationMessage, inputAreaHeight(), groupViewModel.isNonAdminInAnnouncementGroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3889,7 +3921,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
boolean initiating = threadId == -1;
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
@@ -3914,33 +3946,23 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
false,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
false);
|
||||
false,
|
||||
null);
|
||||
}
|
||||
|
||||
private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
|
||||
@Override
|
||||
public void onDismissed(final List<IdentityRecord> unverifiedIdentities) {
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : unverifiedIdentities) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
VerifiedStatus.DEFAULT);
|
||||
}
|
||||
SimpleTask.run(() -> {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : unverifiedIdentities) {
|
||||
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
VerifiedStatus.DEFAULT);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
initializeIdentityRecords();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
return null;
|
||||
}, nothing -> initializeIdentityRecords());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -36,7 +37,7 @@ import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -45,6 +46,7 @@ import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||
@@ -53,19 +55,17 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -106,17 +106,17 @@ public class ConversationAdapter
|
||||
private static final long FOOTER_ID = Long.MIN_VALUE + 1;
|
||||
|
||||
private final ItemClickListener clickListener;
|
||||
private final Context context;
|
||||
private final LifecycleOwner lifecycleOwner;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Locale locale;
|
||||
private final Recipient recipient;
|
||||
|
||||
private final Set<ConversationMessage> selected;
|
||||
private final Set<MultiselectPart> selected;
|
||||
private final List<ConversationMessage> fastRecords;
|
||||
private final Set<Long> releasedFastRecords;
|
||||
private final Calendar calendar;
|
||||
private final MessageDigest digest;
|
||||
private final AttachmentMediaSourceFactory attachmentMediaSourceFactory;
|
||||
|
||||
private String searchQuery;
|
||||
private ConversationMessage recordToPulse;
|
||||
@@ -128,12 +128,12 @@ public class ConversationAdapter
|
||||
private ConversationMessage inlineContent;
|
||||
private Colorizer colorizer;
|
||||
|
||||
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
|
||||
ConversationAdapter(@NonNull Context context,
|
||||
@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
@NonNull Colorizer colorizer)
|
||||
{
|
||||
super(new DiffUtil.ItemCallback<ConversationMessage>() {
|
||||
@@ -149,6 +149,7 @@ public class ConversationAdapter
|
||||
});
|
||||
|
||||
this.lifecycleOwner = lifecycleOwner;
|
||||
this.context = context;
|
||||
|
||||
this.glideRequests = glideRequests;
|
||||
this.locale = locale;
|
||||
@@ -161,7 +162,6 @@ public class ConversationAdapter
|
||||
this.digest = getMessageDigestOrThrow();
|
||||
this.hasWallpaper = recipient.hasWallpaper();
|
||||
this.isMessageRequestAccepted = true;
|
||||
this.attachmentMediaSourceFactory = attachmentMediaSourceFactory;
|
||||
this.colorizer = colorizer;
|
||||
|
||||
setHasStableIds(true);
|
||||
@@ -185,9 +185,9 @@ public class ConversationAdapter
|
||||
} else if (messageRecord.isUpdate()) {
|
||||
return MESSAGE_TYPE_UPDATE;
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
return messageRecord.isMms() ? MESSAGE_TYPE_OUTGOING_MULTIMEDIA : MESSAGE_TYPE_OUTGOING_TEXT;
|
||||
return MessageRecordUtil.isTextOnly(messageRecord, context) ? MESSAGE_TYPE_OUTGOING_TEXT : MESSAGE_TYPE_OUTGOING_MULTIMEDIA;
|
||||
} else {
|
||||
return messageRecord.isMms() ? MESSAGE_TYPE_INCOMING_MULTIMEDIA : MESSAGE_TYPE_INCOMING_TEXT;
|
||||
return MessageRecordUtil.isTextOnly(messageRecord, context) ? MESSAGE_TYPE_INCOMING_TEXT : MESSAGE_TYPE_INCOMING_MULTIMEDIA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@ public class ConversationAdapter
|
||||
return message.getUniqueId(digest);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
@@ -218,19 +219,20 @@ public class ConversationAdapter
|
||||
case MESSAGE_TYPE_OUTGOING_TEXT:
|
||||
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
|
||||
BindableConversationItem bindable = (BindableConversationItem) itemView;
|
||||
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
|
||||
BindableConversationItem bindable = (BindableConversationItem) itemView;
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
itemView.setOnClickListener((v) -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemClick(bindable.getConversationMessage());
|
||||
clickListener.onItemClick(bindable.getMultiselectPartForLatestTouch());
|
||||
}
|
||||
});
|
||||
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
itemView.setOnLongClickListener((v) -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemLongClick(itemView, bindable.getConversationMessage());
|
||||
clickListener.onItemLongClick(itemView, bindable.getMultiselectPartForLatestTouch());
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -294,7 +296,6 @@ public class ConversationAdapter
|
||||
conversationMessage == recordToPulse,
|
||||
hasWallpaper,
|
||||
isMessageRequestAccepted,
|
||||
attachmentMediaSourceFactory,
|
||||
conversationMessage == inlineContent,
|
||||
colorizer);
|
||||
|
||||
@@ -338,8 +339,8 @@ public class ConversationAdapter
|
||||
|
||||
if (conversationMessage == null) return -1;
|
||||
|
||||
calendar.setTime(new Date(conversationMessage.getMessageRecord().getDateSent()));
|
||||
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
|
||||
calendar.setTimeInMillis(conversationMessage.getMessageRecord().getDateReceived());
|
||||
return calendar.get(Calendar.YEAR) * 1000L + calendar.get(Calendar.DAY_OF_YEAR);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -352,7 +353,7 @@ public class ConversationAdapter
|
||||
Context context = viewHolder.itemView.getContext();
|
||||
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
||||
|
||||
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived()));
|
||||
viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived()));
|
||||
|
||||
if (type == HEADER_TYPE_POPOVER_DATE) {
|
||||
if (hasWallpaper) {
|
||||
@@ -555,10 +556,14 @@ public class ConversationAdapter
|
||||
/**
|
||||
* Returns set of records that are selected in multi-select mode.
|
||||
*/
|
||||
Set<ConversationMessage> getSelectedItems() {
|
||||
public Set<MultiselectPart> getSelectedItems() {
|
||||
return new HashSet<>(selected);
|
||||
}
|
||||
|
||||
public void removeFromSelection(@NonNull Set<MultiselectPart> parts) {
|
||||
selected.removeAll(parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all selected records from multi-select mode.
|
||||
*/
|
||||
@@ -569,11 +574,11 @@ public class ConversationAdapter
|
||||
/**
|
||||
* Toggles the selected state of a record in multi-select mode.
|
||||
*/
|
||||
void toggleSelection(ConversationMessage conversationMessage) {
|
||||
if (selected.contains(conversationMessage)) {
|
||||
selected.remove(conversationMessage);
|
||||
void toggleSelection(MultiselectPart multiselectPart) {
|
||||
if (selected.contains(multiselectPart)) {
|
||||
selected.remove(multiselectPart);
|
||||
} else {
|
||||
selected.add(conversationMessage);
|
||||
selected.add(multiselectPart);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,9 +587,9 @@ public class ConversationAdapter
|
||||
*/
|
||||
@MainThread
|
||||
static void initializePool(@NonNull RecyclerView.RecycledViewPool pool) {
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_TEXT, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_TEXT, 25);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_MULTIMEDIA, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_TEXT, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_TEXT, 25);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_MULTIMEDIA, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_PLACEHOLDER, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_HEADER, 1);
|
||||
@@ -690,8 +695,8 @@ public class ConversationAdapter
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable MediaSource getMediaSource() {
|
||||
return getBindable().getMediaSource();
|
||||
public @Nullable MediaItem getMediaItem() {
|
||||
return getBindable().getMediaItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -699,9 +704,9 @@ public class ConversationAdapter
|
||||
return getBindable().getPlaybackPolicyEnforcer();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public @Override Projection getProjection(@NonNull ViewGroup recyclerView) {
|
||||
return getBindable().getProjection(recyclerView);
|
||||
@Override
|
||||
public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) {
|
||||
return getBindable().getGiphyMp4PlayableProjection(recyclerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -782,7 +787,7 @@ public class ConversationAdapter
|
||||
}
|
||||
|
||||
interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||
void onItemClick(ConversationMessage item);
|
||||
void onItemLongClick(View itemView, ConversationMessage item);
|
||||
void onItemClick(MultiselectPart item);
|
||||
void onItemLongClick(View itemView, MultiselectPart item);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user