mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-05 09:48:54 +01:00
Compare commits
323 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a015fa13 | ||
|
|
233ba03f73 | ||
|
|
c547553770 | ||
|
|
0a5f852c09 | ||
|
|
ddf59fb45a | ||
|
|
5a6d77bae4 | ||
|
|
ae0d6b5926 | ||
|
|
9917b5d7b4 | ||
|
|
0558d5f0b3 | ||
|
|
597cf3f576 | ||
|
|
65af5f0849 | ||
|
|
cff5df4353 | ||
|
|
855bada9b8 | ||
|
|
9802724baa | ||
|
|
14db5ce349 | ||
|
|
bb1e6ffae0 | ||
|
|
210bb23aa4 | ||
|
|
de3a6a85c9 | ||
|
|
7ef41c0169 | ||
|
|
d08f1b65d0 | ||
|
|
5de05edaa1 | ||
|
|
b556967240 | ||
|
|
80a2e1e3cc | ||
|
|
b91a2e1450 | ||
|
|
45e406013a | ||
|
|
deb53e1751 | ||
|
|
601eb967de | ||
|
|
5c03608c8f | ||
|
|
0877d6a25e | ||
|
|
83ee4c0147 | ||
|
|
c09c6587b9 | ||
|
|
6617ecdf39 | ||
|
|
b36b34b1fd | ||
|
|
d8e0baa9ee | ||
|
|
3bb4cdf46b | ||
|
|
2181e34e6a | ||
|
|
d0ca769351 | ||
|
|
a090b07b1c | ||
|
|
178f5e80e3 | ||
|
|
d7bf4f178f | ||
|
|
dd9632da5b | ||
|
|
e235ec4129 | ||
|
|
988728be3e | ||
|
|
e2d86067cc | ||
|
|
b447f98f45 | ||
|
|
3e7f63af43 | ||
|
|
fdeed850b0 | ||
|
|
5c1d4d289f | ||
|
|
d19cba049d | ||
|
|
19ed3cb9ea | ||
|
|
cbb23b3d6c | ||
|
|
3c8c04d9e5 | ||
|
|
c3b792e4cf | ||
|
|
8f6998a8f6 | ||
|
|
49f66a31ff | ||
|
|
ec34604ffc | ||
|
|
8af7c5043a | ||
|
|
de1fbcf696 | ||
|
|
c4c43ee958 | ||
|
|
a2bf15d105 | ||
|
|
393ee545c0 | ||
|
|
959bbdae6c | ||
|
|
9f474fadf4 | ||
|
|
007e8a9dca | ||
|
|
b081452bed | ||
|
|
45668e4048 | ||
|
|
f0bf0784e4 | ||
|
|
a05776551f | ||
|
|
24a875c73a | ||
|
|
f0414922be | ||
|
|
bfae20941a | ||
|
|
be47e9e928 | ||
|
|
7d627ee8be | ||
|
|
95276b0192 | ||
|
|
92978b0e3f | ||
|
|
7d7db1b60a | ||
|
|
c5c915d446 | ||
|
|
bf28dfee66 | ||
|
|
f091502949 | ||
|
|
9b0dec7ece | ||
|
|
d690a52fd7 | ||
|
|
cf0d54d04f | ||
|
|
39169784b0 | ||
|
|
8348badcd6 | ||
|
|
9114dc83d7 | ||
|
|
87608c6d3a | ||
|
|
5acbe260e9 | ||
|
|
5e31eb5565 | ||
|
|
7a241e5fb5 | ||
|
|
7e299157ec | ||
|
|
1b1001b0e9 | ||
|
|
45a91e0896 | ||
|
|
91c7e0a0ee | ||
|
|
1a1213d043 | ||
|
|
b5f6513917 | ||
|
|
befb720eda | ||
|
|
9569b6ab4a | ||
|
|
94078f8b91 | ||
|
|
537a1fa2ea | ||
|
|
d6acd5ef36 | ||
|
|
08d9aa0947 | ||
|
|
355a498b9b | ||
|
|
e4d43ade93 | ||
|
|
d254d24d77 | ||
|
|
da34f9e989 | ||
|
|
5de9653149 | ||
|
|
4de8807297 | ||
|
|
ccc08e651c | ||
|
|
fd86dd3424 | ||
|
|
89271ecce2 | ||
|
|
af3a39d64e | ||
|
|
125ff83bac | ||
|
|
33f4bb0000 | ||
|
|
dd7a2834bc | ||
|
|
b0bf077797 | ||
|
|
ef9f1e9884 | ||
|
|
5423ed1d91 | ||
|
|
28c446aa2e | ||
|
|
d0042b1f7d | ||
|
|
62933ba887 | ||
|
|
92884fb3bf | ||
|
|
ee831b0221 | ||
|
|
e96ff92029 | ||
|
|
ade72b9911 | ||
|
|
053b19846b | ||
|
|
8e5500826c | ||
|
|
2f0a528c0f | ||
|
|
840e47a2de | ||
|
|
79a4ceedf9 | ||
|
|
3daa894988 | ||
|
|
1d14a90ac3 | ||
|
|
e273f914b6 | ||
|
|
96844f046f | ||
|
|
926f5b3cdf | ||
|
|
a0031298d8 | ||
|
|
523e21f3be | ||
|
|
15254ee720 | ||
|
|
8648c74221 | ||
|
|
c0ed6b1d41 | ||
|
|
1641d501c9 | ||
|
|
2dd887cd17 | ||
|
|
373fa1faec | ||
|
|
35c5a8106d | ||
|
|
642d37edb2 | ||
|
|
35d0f1fc8c | ||
|
|
78acc485fc | ||
|
|
6e71514209 | ||
|
|
22c396067d | ||
|
|
4f03c98f60 | ||
|
|
95cb80a93a | ||
|
|
14886ce28e | ||
|
|
a641020ec0 | ||
|
|
9f622bd689 | ||
|
|
6919e352d6 | ||
|
|
fd6a2c6b10 | ||
|
|
ab34a9b027 | ||
|
|
08db07e960 | ||
|
|
b2038e4ca0 | ||
|
|
c48ea68e7e | ||
|
|
c548816daa | ||
|
|
c5028720e3 | ||
|
|
35f9437413 | ||
|
|
b2b51e63be | ||
|
|
afd6af6f57 | ||
|
|
9ba5660f5b | ||
|
|
8aefd59eaa | ||
|
|
7203228626 | ||
|
|
112f4bb281 | ||
|
|
c7fb0e2ab8 | ||
|
|
f6cd7b1f3c | ||
|
|
d40254aa69 | ||
|
|
75a13aa22a | ||
|
|
5a884d8fc8 | ||
|
|
b5dcf8e8f1 | ||
|
|
bfdedd57d1 | ||
|
|
2b021f5237 | ||
|
|
791c1ee8dd | ||
|
|
c2f953b097 | ||
|
|
4984cc8eb4 | ||
|
|
23e4856c5e | ||
|
|
e50787ae20 | ||
|
|
64e4bcf46a | ||
|
|
693a82f133 | ||
|
|
79d73c9e74 | ||
|
|
5a51544cae | ||
|
|
bf2ab74ca4 | ||
|
|
d39ec479ba | ||
|
|
48fa81a8b8 | ||
|
|
2de96dcfbf | ||
|
|
01047e90ad | ||
|
|
cd4320c0ef | ||
|
|
c55b0357f1 | ||
|
|
7551dd77c5 | ||
|
|
2f3c7097a9 | ||
|
|
9de519eb3d | ||
|
|
c0008f7383 | ||
|
|
08cb0967c9 | ||
|
|
50b37e0402 | ||
|
|
89b918fbd2 | ||
|
|
856bd54059 | ||
|
|
3943e670b2 | ||
|
|
917744f091 | ||
|
|
552fdcce98 | ||
|
|
e1151bfce4 | ||
|
|
37b0d3d755 | ||
|
|
b68bd9179c | ||
|
|
ea92280cea | ||
|
|
8521b87147 | ||
|
|
829c06ab1a | ||
|
|
a8d9933265 | ||
|
|
62f5088553 | ||
|
|
3922bfacf5 | ||
|
|
4e67752850 | ||
|
|
7ff2b1ab33 | ||
|
|
34f679b10b | ||
|
|
449acaf9df | ||
|
|
d52c66d601 | ||
|
|
47134e19f1 | ||
|
|
0aabf9945f | ||
|
|
8bc7d1b7f5 | ||
|
|
4dae424a5c | ||
|
|
ee48a1ae25 | ||
|
|
e8882a8076 | ||
|
|
e48c1bf207 | ||
|
|
d1eab086f1 | ||
|
|
e41c73f293 | ||
|
|
3eb8db00aa | ||
|
|
bbadda5656 | ||
|
|
92df5b9564 | ||
|
|
e0b892b630 | ||
|
|
1a499e23d9 | ||
|
|
f0d40685df | ||
|
|
4cbed24244 | ||
|
|
0d0c74f358 | ||
|
|
0dd2397fb4 | ||
|
|
3781e1dd60 | ||
|
|
ae40a65924 | ||
|
|
8968ef1b85 | ||
|
|
25ab9a5ad6 | ||
|
|
5c27842a01 | ||
|
|
49a1a4a123 | ||
|
|
ac90eeb42f | ||
|
|
302e653d2f | ||
|
|
3d6ffe25f0 | ||
|
|
363eb22462 | ||
|
|
b04ae3a8b3 | ||
|
|
e6451db888 | ||
|
|
2bbceaabd3 | ||
|
|
fefbf595cd | ||
|
|
85b3947150 | ||
|
|
a0d70a955a | ||
|
|
a5c2595796 | ||
|
|
4193f7bcbd | ||
|
|
5102f5215c | ||
|
|
bdd48629c6 | ||
|
|
fde1e5ab77 | ||
|
|
46dd7f8a06 | ||
|
|
282639469d | ||
|
|
bad1cc1571 | ||
|
|
3757449b8f | ||
|
|
0af65d1367 | ||
|
|
adcb1bae13 | ||
|
|
b69ffe4e15 | ||
|
|
8c34357cc6 | ||
|
|
a17fd447a7 | ||
|
|
eaf72b194f | ||
|
|
09a3391761 | ||
|
|
e0e25da6a9 | ||
|
|
90d4069d0a | ||
|
|
0dcae81dba | ||
|
|
9177f5637a | ||
|
|
5918227bff | ||
|
|
dd79688f48 | ||
|
|
dbce4be31d | ||
|
|
4275877b47 | ||
|
|
11221315e4 | ||
|
|
a4f44a96fd | ||
|
|
ba54051f8c | ||
|
|
130b796564 | ||
|
|
a15ba60252 | ||
|
|
8014a70134 | ||
|
|
4f73e36d72 | ||
|
|
68bd9c6e1e | ||
|
|
20d2c43356 | ||
|
|
b94624fd5a | ||
|
|
4ae129d2af | ||
|
|
158505c8a8 | ||
|
|
c98fd1a452 | ||
|
|
c6d0ef218a | ||
|
|
ba5b3e01f2 | ||
|
|
e84ae83c28 | ||
|
|
33eeca9e3e | ||
|
|
6288dc19e9 | ||
|
|
b9ba1a3568 | ||
|
|
93270b90df | ||
|
|
a0235cbc6c | ||
|
|
28f0724d90 | ||
|
|
08a305cb0f | ||
|
|
50d2faf381 | ||
|
|
49b9d5c3aa | ||
|
|
7385112115 | ||
|
|
3feb73789d | ||
|
|
b80c844a0b | ||
|
|
755a25519a | ||
|
|
bbb9eab148 | ||
|
|
c8e62e5f60 | ||
|
|
19818443ff | ||
|
|
c30a43ef45 | ||
|
|
76539ff0f2 | ||
|
|
39f4ca10ef | ||
|
|
3d77ce0d57 | ||
|
|
3b9cfc8e5a | ||
|
|
761d70851c | ||
|
|
4c28619010 | ||
|
|
884710fc30 | ||
|
|
2e96042578 | ||
|
|
5b49be47f9 | ||
|
|
d6a42daef7 | ||
|
|
d5679ef95f | ||
|
|
60e54fb2af | ||
|
|
575e00dcf8 | ||
|
|
a8a104242a | ||
|
|
372b0d9f2b |
0
.github/stale.yml
vendored
Normal file
0
.github/stale.yml
vendored
Normal file
8
.idea/file.template.settings.xml
generated
Normal file
8
.idea/file.template.settings.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExportableFileTemplateSettings">
|
||||
<default_templates>
|
||||
<template name="ViewModel.kt" file-name="${NAME}ViewModel" reformat="true" live-template-enabled="false" />
|
||||
</default_templates>
|
||||
</component>
|
||||
</project>
|
||||
20
.idea/fileTemplates/ViewModel.kt
generated
Normal file
20
.idea/fileTemplates/ViewModel.kt
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
class ${NAME}ViewModel : ViewModel() {
|
||||
|
||||
private val store = Store(${NAME}State())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: LiveData<${NAME}State> = store.stateLiveData
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'androidx.navigation.safeargs'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
apply from: 'translations.gradle'
|
||||
apply from: 'witness-verifications.gradle'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'app.cash.exhaustive'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
@@ -47,7 +44,7 @@ repositories {
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.11.4'
|
||||
artifact = 'com.google.protobuf:protoc:3.18.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
@@ -60,8 +57,13 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 973
|
||||
def canonicalVersionName = "5.28.0"
|
||||
ktlint {
|
||||
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
|
||||
version = "0.43.2"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1005
|
||||
def canonicalVersionName = "5.32.2"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -84,8 +86,6 @@ def selectableVariants = [
|
||||
'playStagingFlipper',
|
||||
'playStagingPerf',
|
||||
'playStagingRelease',
|
||||
'studyProdMock',
|
||||
'studyProdPerf',
|
||||
'websiteProdFlipper',
|
||||
'websiteProdRelease',
|
||||
]
|
||||
@@ -148,10 +148,14 @@ android {
|
||||
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
|
||||
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] {" +
|
||||
"new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" +
|
||||
"}"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXQ==\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
@@ -214,8 +218,6 @@ android {
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
exclude '/org/spongycastle/x509/CertPathReviewerMessages.properties'
|
||||
exclude '/org/spongycastle/x509/CertPathReviewerMessages_de.properties'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -224,7 +226,7 @@ android {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
isDefault true
|
||||
minifyEnabled true
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||
'proguard/proguard-firebase-messaging.pro',
|
||||
'proguard/proguard-google-play-services.pro',
|
||||
@@ -233,7 +235,6 @@ android {
|
||||
'proguard/proguard-appcompat-v7.pro',
|
||||
'proguard/proguard-square-okhttp.pro',
|
||||
'proguard/proguard-square-okio.pro',
|
||||
'proguard/proguard-spongycastle.pro',
|
||||
'proguard/proguard-rounded-image-view.pro',
|
||||
'proguard/proguard-glide.pro',
|
||||
'proguard/proguard-shortcutbadger.pro',
|
||||
@@ -265,16 +266,10 @@ android {
|
||||
initWith debug
|
||||
isDefault false
|
||||
debuggable false
|
||||
minifyEnabled true
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
|
||||
}
|
||||
mock {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Mock\""
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
@@ -304,16 +299,6 @@ android {
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\""
|
||||
}
|
||||
|
||||
study {
|
||||
dimension 'distribution'
|
||||
|
||||
applicationIdSuffix ".study"
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"study\""
|
||||
}
|
||||
|
||||
prod {
|
||||
dimension 'environment'
|
||||
|
||||
@@ -335,10 +320,14 @@ android {
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
|
||||
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] {" +
|
||||
"new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" +
|
||||
"}"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==\""
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||
@@ -355,6 +344,9 @@ android {
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
|
||||
def tag = getCurrentGitTag()
|
||||
if (tag != null && tag.length() > 0) {
|
||||
if (tag.startsWith("v")) {
|
||||
tag = tag.substring(1)
|
||||
}
|
||||
output.versionNameOverride = tag
|
||||
}
|
||||
} else {
|
||||
@@ -381,6 +373,7 @@ android {
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
disable "LintError"
|
||||
@@ -448,6 +441,7 @@ dependencies {
|
||||
implementation project(':libsignal-service')
|
||||
implementation project(':paging')
|
||||
implementation project(':core-util')
|
||||
implementation project(':glide-config')
|
||||
implementation project(':video')
|
||||
implementation project(':device-transfer')
|
||||
implementation project(':image-editor')
|
||||
@@ -474,8 +468,6 @@ dependencies {
|
||||
implementation libs.apache.httpclient.android
|
||||
implementation libs.photoview
|
||||
implementation libs.glide.glide
|
||||
kapt libs.glide.compiler
|
||||
kapt libs.androidx.annotation
|
||||
implementation libs.roundedimageview
|
||||
implementation libs.materialish.progress
|
||||
implementation libs.greenrobot.eventbus
|
||||
@@ -534,6 +526,9 @@ dependencies {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation testLibs.robolectric.shadows.multidex
|
||||
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) {
|
||||
force = true
|
||||
}
|
||||
testImplementation testLibs.hamcrest.hamcrest
|
||||
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
@@ -554,10 +549,6 @@ dependencies {
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return System.currentTimeMillis().toString()
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* When writing tests, be very careful to call [DatabaseObserver.flush] before asserting any observer state. Internally, the observer is enqueueing tasks on
|
||||
* an executor, and failing to flush the executor will lead to incorrect/flaky tests.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DatabaseObserverTest {
|
||||
|
||||
private lateinit var db: SQLiteDatabase
|
||||
private lateinit var observer: DatabaseObserver
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.instance!!.signalWritableDatabase
|
||||
observer = ApplicationDependencies.getDatabaseObserver()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_runsImmediatelyIfNotInTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_runsAfterSuccessIfInTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
observer.flush()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_doesNotRunAfterFailedTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.endTransaction()
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
// Verifying we still don't run it even after a subsequent success
|
||||
db.beginTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
observer.flush()
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_onlyRunAfterAllTransactionsComplete() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.beginTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
observer.flush()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_runsImmediatelyIfTheTransactionIsOnAnotherThread() {
|
||||
db.beginTransaction()
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertTrue(hasRun.get())
|
||||
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
latch.await()
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_runsAfterSuccessIfInTransaction_ignoreDuplicateNotifications() {
|
||||
val thread1Count = AtomicInteger(0)
|
||||
val thread2Count = AtomicInteger(0)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
observer.registerConversationObserver(1) { thread1Count.incrementAndGet() }
|
||||
observer.registerConversationObserver(2) { thread2Count.incrementAndGet() }
|
||||
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.notifyConversationListeners(2)
|
||||
observer.notifyConversationListeners(2)
|
||||
|
||||
observer.flush()
|
||||
assertEquals(0, thread1Count.get())
|
||||
assertEquals(0, thread2Count.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
observer.flush()
|
||||
assertEquals(1, thread1Count.get())
|
||||
assertEquals(1, thread2Count.get())
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
|
||||
import org.thoughtcrime.securesms.keyvalue.AccountValues
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||
@@ -17,6 +21,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.UUID
|
||||
|
||||
@@ -189,17 +194,21 @@ class RecipientDatabaseTest {
|
||||
@Test
|
||||
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_highTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
recipientDatabase.setPni(existingId, PNI_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||
recipientDatabase.setPni(retrievedId, PNI_A)
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
assertEquals(PNI_A, retrievedRecipient.pni.get())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireAci())
|
||||
assertFalse(existingRecipient.hasE164())
|
||||
assertNull(existingRecipient.pni.orNull())
|
||||
}
|
||||
|
||||
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. And low trust means we can’t take the e164. */
|
||||
@@ -262,8 +271,11 @@ class RecipientDatabaseTest {
|
||||
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
|
||||
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
val changeNumberListener = ChangeNumberListener()
|
||||
changeNumberListener.enqueue()
|
||||
|
||||
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
@@ -274,6 +286,32 @@ class RecipientDatabaseTest {
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
assertEquals(retrievedId, existingE164Recipient.id)
|
||||
|
||||
changeNumberListener.waitForJobManager()
|
||||
assertFalse(changeNumberListener.numberChangeWasEnqueued)
|
||||
}
|
||||
|
||||
/** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust], but with a number change. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust_changedNumber() {
|
||||
val changeNumberListener = ChangeNumberListener()
|
||||
changeNumberListener.enqueue()
|
||||
|
||||
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
assertEquals(retrievedId, existingE164Recipient.id)
|
||||
|
||||
changeNumberListener.waitForJobManager()
|
||||
assert(changeNumberListener.numberChangeWasEnqueued)
|
||||
}
|
||||
|
||||
/** Low trust means you can’t merge. If you’re retrieving a user from the table with this data, prefer the ACI one. */
|
||||
@@ -297,6 +335,9 @@ class RecipientDatabaseTest {
|
||||
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() {
|
||||
val changeNumberListener = ChangeNumberListener()
|
||||
changeNumberListener.enqueue()
|
||||
|
||||
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||
|
||||
@@ -310,6 +351,8 @@ class RecipientDatabaseTest {
|
||||
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||
assertEquals(ACI_B, existingRecipient2.requireAci())
|
||||
assertFalse(existingRecipient2.hasE164())
|
||||
|
||||
assert(changeNumberListener.numberChangeWasEnqueued)
|
||||
}
|
||||
|
||||
/** Another low trust case. No new rules here, just a more complex scenario to show how different rules interact. */
|
||||
@@ -376,6 +419,63 @@ class RecipientDatabaseTest {
|
||||
assertEquals(E164_A, recipientWithId1.requireE164())
|
||||
}
|
||||
|
||||
/** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfFalse() {
|
||||
val dataSet = KeyValueDataSet().apply {
|
||||
putString(AccountValues.KEY_E164, E164_A)
|
||||
putString(AccountValues.KEY_ACI, ACI_A.toString())
|
||||
}
|
||||
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
|
||||
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = false)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfTrue() {
|
||||
val dataSet = KeyValueDataSet().apply {
|
||||
putString(AccountValues.KEY_E164, E164_A)
|
||||
putString(AccountValues.KEY_ACI, ACI_A.toString())
|
||||
}
|
||||
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
|
||||
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** Verifying a case where a change number job is expected to be enqueued. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_highTrust_changedNumber() {
|
||||
val changeNumberListener = ChangeNumberListener()
|
||||
changeNumberListener.enqueue()
|
||||
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
|
||||
changeNumberListener.waitForJobManager()
|
||||
assert(changeNumberListener.numberChangeWasEnqueued)
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// Misc
|
||||
// ==============================================================
|
||||
@@ -426,10 +526,31 @@ class RecipientDatabaseTest {
|
||||
}
|
||||
}
|
||||
|
||||
private class ChangeNumberListener {
|
||||
|
||||
var numberChangeWasEnqueued = false
|
||||
private set
|
||||
|
||||
fun waitForJobManager() {
|
||||
ApplicationDependencies.getJobManager().flush()
|
||||
ThreadUtil.sleep(500)
|
||||
}
|
||||
|
||||
fun enqueue() {
|
||||
ApplicationDependencies.getJobManager().addListener(
|
||||
{ job -> job.factoryKey == RecipientChangedNumberJob.KEY },
|
||||
{ _, _ -> numberChangeWasEnqueued = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
|
||||
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
|
||||
|
||||
const val E164_A = "+12221234567"
|
||||
const val E164_B = "+13331234567"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* These are tests for the wrapper we wrote around SQLCipherDatabase, not the stock or SQLCipher one.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SQLiteDatabaseTest {
|
||||
|
||||
private lateinit var db: SQLiteDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.instance!!.signalWritableDatabase
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsImmediatelyIfNotInTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAfterSuccessIfInTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_doesNotRunAfterFailedTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.endTransaction()
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
// Verifying we still don't run it even after a subsequent success
|
||||
db.beginTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_onlyRunAfterAllTransactionsComplete() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.beginTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsImmediatelyIfTheTransactionIsOnAnotherThread() {
|
||||
db.beginTransaction()
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertTrue(hasRun.get())
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
latch.await()
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAfterSuccessIfInTransaction_ignoreDuplicates() {
|
||||
val hasRun1 = AtomicBoolean(false)
|
||||
val hasRun2 = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction("key") { hasRun1.set(true) }
|
||||
db.runPostSuccessfulTransaction("key") { hasRun2.set(true) }
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
}
|
||||
}
|
||||
@@ -380,7 +380,7 @@
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".VerifyIdentityActivity"
|
||||
<activity android:name=".verify.VerifyIdentityActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -428,7 +428,7 @@
|
||||
<activity android:name=".registration.RegistrationNavigationActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".revealable.ViewOnceMessageActivity"
|
||||
|
||||
@@ -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 {
|
||||
@@ -13,12 +12,13 @@ public final class AppCapabilities {
|
||||
private static final boolean GV1_MIGRATION = true;
|
||||
private static final boolean ANNOUNCEMENT_GROUPS = true;
|
||||
private static final boolean SENDER_KEY = true;
|
||||
private static final boolean CHANGE_NUMBER = 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, SENDER_KEY, ANNOUNCEMENT_GROUPS, FeatureFlags.changeNumber());
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,9 @@ public final class AppInitialization {
|
||||
InsightsOptOut.userRequestedOptOut(context);
|
||||
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
|
||||
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, true);
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
|
||||
@@ -73,10 +72,9 @@ public final class AppInitialization {
|
||||
InsightsOptOut.userRequestedOptOut(context);
|
||||
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
|
||||
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
|
||||
@@ -35,6 +35,9 @@ import org.signal.core.util.logging.AndroidLogger;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
@@ -62,6 +65,7 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
@@ -79,7 +83,6 @@ 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.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -168,6 +171,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
})
|
||||
.addBlocking("blob-provider", this::initializeBlobProvider)
|
||||
.addBlocking("feature-flags", FeatureFlags::init)
|
||||
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
@@ -190,6 +194,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
|
||||
.addPostRender(RetrieveReleaseChannelJob::enqueue)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -211,7 +217,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getShakeToReport().enable();
|
||||
|
||||
@@ -92,6 +92,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
|
||||
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
|
||||
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
|
||||
void onCallToAction(@NonNull String action);
|
||||
void onDonateClicked();
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
@@ -76,6 +76,11 @@ public final class BlockUnblockDialog {
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_block, ((dialog, which) -> onBlock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
} else if (recipient.isReleaseNotes()) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_block_getting_signal_updates_and_news);
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
} else {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
|
||||
@@ -115,6 +120,12 @@ public final class BlockUnblockDialog {
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
} else if (recipient.isReleaseNotes()) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_resume_getting_signal_updates_and_news);
|
||||
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
} else {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other);
|
||||
|
||||
@@ -746,7 +746,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
return;
|
||||
}
|
||||
|
||||
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
|
||||
AutoTransition transition = new AutoTransition();
|
||||
transition.setDuration(CHIP_GROUP_REVEAL_DURATION_MS);
|
||||
transition.excludeChildren(recyclerView, true);
|
||||
transition.excludeTarget(recyclerView, true);
|
||||
TransitionManager.beginDelayedTransition(constraintLayout, transition);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(constraintLayout);
|
||||
@@ -765,8 +769,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
/** 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 onBeforeContactSelected(Optional<RecipientId> recipientId, @Nullable String number, Consumer<Boolean> callback);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, @Nullable String number);
|
||||
void onSelectionChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQrDataFound(final String data) {
|
||||
public void onQrDataFound(@NonNull final String data) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
|
||||
Uri uri = Uri.parse(data);
|
||||
|
||||
@@ -73,6 +73,7 @@ import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
@@ -534,7 +535,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
public static boolean isContentTypeSupported(final String contentType) {
|
||||
return contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video/"));
|
||||
return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -67,7 +67,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
|
||||
if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(this)) {
|
||||
if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(getApplication())) {
|
||||
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
|
||||
|
||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||
|
||||
@@ -1,774 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2017 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.TypeEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnticipateInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextSwitcher;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.OneShotPreDrawListener;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
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.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;
|
||||
import org.thoughtcrime.securesms.qr.QrCode;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
|
||||
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Activity for verifying identity keys.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class VerifyIdentityActivity extends PassphraseRequiredActivity implements ScanListener, View.OnClickListener {
|
||||
|
||||
private static final String TAG = Log.tag(VerifyIdentityActivity.class);
|
||||
|
||||
private static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
private static final String IDENTITY_EXTRA = "recipient_identity";
|
||||
private static final String VERIFIED_EXTRA = "verified_state";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
|
||||
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityRecord identityRecord)
|
||||
{
|
||||
return newIntent(context,
|
||||
identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
identityRecord.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
|
||||
}
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityRecord identityRecord,
|
||||
boolean verified)
|
||||
{
|
||||
return newIntent(context,
|
||||
identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
verified);
|
||||
}
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull RecipientId recipientId,
|
||||
@NonNull IdentityKey identityKey,
|
||||
boolean verified)
|
||||
{
|
||||
Intent intent = new Intent(context, VerifyIdentityActivity.class);
|
||||
|
||||
intent.putExtra(RECIPIENT_EXTRA, recipientId);
|
||||
intent.putExtra(IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey));
|
||||
intent.putExtra(VERIFIED_EXTRA, verified);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
|
||||
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
|
||||
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, Recipient.self().requireE164());
|
||||
extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
|
||||
|
||||
scanFragment.setScanListener(this);
|
||||
displayFragment.setClickListener(this);
|
||||
|
||||
initFragment(android.R.id.content, displayFragment, Locale.getDefault(), extras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: finish(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQrDataFound(final String data) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
|
||||
|
||||
getSupportFragmentManager().popBackStack();
|
||||
displayFragment.setScannedFingerprint(data);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
transaction.setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom,
|
||||
R.anim.slide_from_bottom, R.anim.slide_to_top);
|
||||
|
||||
transaction.replace(android.R.id.content, scanFragment)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener {
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
public static final String REMOTE_NUMBER = "remote_number";
|
||||
public static final String REMOTE_IDENTITY = "remote_identity";
|
||||
public static final String LOCAL_IDENTITY = "local_identity";
|
||||
public static final String LOCAL_NUMBER = "local_number";
|
||||
public static final String VERIFIED_STATE = "verified_state";
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private IdentityKey localIdentity;
|
||||
private IdentityKey remoteIdentity;
|
||||
private Fingerprint fingerprint;
|
||||
|
||||
private Toolbar toolbar;
|
||||
private ScrollView scrollView;
|
||||
private View container;
|
||||
private View numbersContainer;
|
||||
private View loading;
|
||||
private View qrCodeContainer;
|
||||
private ImageView qrCode;
|
||||
private ImageView qrVerified;
|
||||
private TextSwitcher tapLabel;
|
||||
private TextView description;
|
||||
private View.OnClickListener clickListener;
|
||||
private Button verifyButton;
|
||||
private View toolbarShadow;
|
||||
private View bottomShadow;
|
||||
|
||||
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.toolbar = container.findViewById(R.id.toolbar);
|
||||
this.scrollView = container.findViewById(R.id.scroll_view);
|
||||
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.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);
|
||||
this.toolbarShadow = container.findViewById(R.id.toolbar_shadow);
|
||||
this.bottomShadow = container.findViewById(R.id.verify_identity_bottom_shadow);
|
||||
this.codes[0] = container.findViewById(R.id.code_first);
|
||||
this.codes[1] = container.findViewById(R.id.code_second);
|
||||
this.codes[2] = container.findViewById(R.id.code_third);
|
||||
this.codes[3] = container.findViewById(R.id.code_fourth);
|
||||
this.codes[4] = container.findViewById(R.id.code_fifth);
|
||||
this.codes[5] = container.findViewById(R.id.code_sixth);
|
||||
this.codes[6] = container.findViewById(R.id.code_seventh);
|
||||
this.codes[7] = container.findViewById(R.id.code_eighth);
|
||||
this.codes[8] = container.findViewById(R.id.code_ninth);
|
||||
this.codes[9] = container.findViewById(R.id.code_tenth);
|
||||
this.codes[10] = container.findViewById(R.id.code_eleventh);
|
||||
this.codes[11] = container.findViewById(R.id.code_twelth);
|
||||
|
||||
this.qrCodeContainer.setOnClickListener(clickListener);
|
||||
this.registerForContextMenu(numbersContainer);
|
||||
|
||||
updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false);
|
||||
this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true)));
|
||||
|
||||
this.scrollView.getViewTreeObserver().addOnScrollChangedListener(this);
|
||||
|
||||
((AppCompatActivity)requireActivity()).setSupportActionBar(toolbar);
|
||||
((AppCompatActivity)requireActivity()).setTitle(R.string.AndroidManifest__verify_safety_number);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override public void onDestroyView() {
|
||||
this.scrollView.getViewTreeObserver().removeOnScrollChangedListener(this);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
RecipientId recipientId = getArguments().getParcelable(RECIPIENT_ID);
|
||||
IdentityKeyParcelable localIdentityParcelable = getArguments().getParcelable(LOCAL_IDENTITY);
|
||||
IdentityKeyParcelable remoteIdentityParcelable = getArguments().getParcelable(REMOTE_IDENTITY);
|
||||
|
||||
if (recipientId == null) throw new AssertionError("RecipientId required");
|
||||
if (localIdentityParcelable == null) throw new AssertionError("local identity required");
|
||||
if (remoteIdentityParcelable == null) throw new AssertionError("remote identity required");
|
||||
|
||||
this.localIdentity = localIdentityParcelable.get();
|
||||
this.recipient = Recipient.live(recipientId);
|
||||
this.remoteIdentity = remoteIdentityParcelable.get();
|
||||
|
||||
int version;
|
||||
byte[] localId;
|
||||
byte[] remoteId;
|
||||
|
||||
//noinspection WrongThread
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
if (FeatureFlags.verifyV2() && resolved.getAci().isPresent()) {
|
||||
Log.i(TAG, "Using UUID (version 2).");
|
||||
version = 2;
|
||||
localId = Recipient.self().requireAci().toByteArray();
|
||||
remoteId = resolved.requireAci().toByteArray();
|
||||
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
|
||||
Log.i(TAG, "Using E164 (version 1).");
|
||||
version = 1;
|
||||
localId = Recipient.self().requireE164().getBytes();
|
||||
remoteId = resolved.requireE164().getBytes();
|
||||
} else {
|
||||
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getAci().isPresent(), resolved.getE164().isPresent()));
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
|
||||
.setOnDismissListener(dialog -> {
|
||||
requireActivity().finish();
|
||||
dialog.dismiss();
|
||||
})
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
this.recipient.observe(this, this::setRecipientText);
|
||||
|
||||
new AsyncTask<Void, Void, Fingerprint>() {
|
||||
@Override
|
||||
protected Fingerprint doInBackground(Void... params) {
|
||||
return new NumericFingerprintGenerator(5200).createFor(version,
|
||||
localId, localIdentity,
|
||||
remoteId, remoteIdentity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Fingerprint fingerprint) {
|
||||
if (getActivity() == null) return;
|
||||
VerifyDisplayFragment.this.fingerprint = fingerprint;
|
||||
setFingerprintViews(fingerprint, true);
|
||||
getActivity().supportInvalidateOptionsMenu();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
setRecipientText(recipient.get());
|
||||
|
||||
if (fingerprint != null) {
|
||||
setFingerprintViews(fingerprint, false);
|
||||
}
|
||||
|
||||
if (animateSuccessOnDraw) {
|
||||
animateSuccessOnDraw = false;
|
||||
animateVerifiedSuccess();
|
||||
} else if (animateFailureOnDraw) {
|
||||
animateFailureOnDraw = false;
|
||||
animateVerifiedFailure();
|
||||
}
|
||||
|
||||
ThreadUtil.postToMain(this::onScrollChanged);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View view,
|
||||
ContextMenuInfo menuInfo)
|
||||
{
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
|
||||
if (fingerprint != null) {
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.verify_display_fragment_context_menu, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
if (fingerprint == null) return super.onContextItemSelected(item);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_copy: handleCopyToClipboard(fingerprint, codes.length); return true;
|
||||
case R.id.menu_compare: handleCompareWithClipboard(fingerprint); return true;
|
||||
default: return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
if (fingerprint != null) {
|
||||
inflater.inflate(R.menu.verify_identity, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.verify_identity__share: handleShare(fingerprint, codes.length); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setScannedFingerprint(String scanned) {
|
||||
try {
|
||||
if (fingerprint.getScannableFingerprint().compareTo(scanned.getBytes("ISO-8859-1"))) {
|
||||
this.animateSuccessOnDraw = true;
|
||||
} else {
|
||||
this.animateFailureOnDraw = true;
|
||||
}
|
||||
} catch (FingerprintVersionMismatchException e) {
|
||||
Log.w(TAG, e);
|
||||
if (e.getOurVersion() < e.getTheirVersion()) {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_a_newer_version_of_Signal, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
this.animateFailureOnDraw = true;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
|
||||
this.animateFailureOnDraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void setClickListener(View.OnClickListener listener) {
|
||||
this.clickListener = listener;
|
||||
}
|
||||
|
||||
private @NonNull String getFormattedSafetyNumbers(@NonNull Fingerprint fingerprint, int segmentCount) {
|
||||
String[] segments = getSegments(fingerprint, segmentCount);
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
result.append(segments[i]);
|
||||
|
||||
if (i != segments.length - 1) {
|
||||
if (((i+1) % 4) == 0) result.append('\n');
|
||||
else result.append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private void handleCopyToClipboard(Fingerprint fingerprint, int segmentCount) {
|
||||
Util.writeTextToClipboard(getActivity(), getFormattedSafetyNumbers(fingerprint, segmentCount));
|
||||
}
|
||||
|
||||
private void handleCompareWithClipboard(Fingerprint fingerprint) {
|
||||
String clipboardData = Util.readTextFromClipboard(getActivity());
|
||||
|
||||
if (clipboardData == null) {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String numericClipboardData = clipboardData.replaceAll("\\D", "");
|
||||
|
||||
if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length() != 60) {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fingerprint.getDisplayableFingerprint().getDisplayText().equals(numericClipboardData)) {
|
||||
animateVerifiedSuccess();
|
||||
} else {
|
||||
animateVerifiedFailure();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleShare(@NonNull Fingerprint fingerprint, int segmentCount) {
|
||||
String shareString =
|
||||
getString(R.string.VerifyIdentityActivity_our_signal_safety_number) + "\n" +
|
||||
getFormattedSafetyNumbers(fingerprint, segmentCount) + "\n";
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, shareString);
|
||||
intent.setType("text/plain");
|
||||
|
||||
try {
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via)));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void setRecipientText(Recipient recipient) {
|
||||
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());
|
||||
}
|
||||
|
||||
private void setFingerprintViews(Fingerprint fingerprint, boolean animate) {
|
||||
String[] segments = getSegments(fingerprint, codes.length);
|
||||
|
||||
for (int i=0;i<codes.length;i++) {
|
||||
if (animate) setCodeSegment(codes[i], segments[i]);
|
||||
else codes[i].setText(segments[i]);
|
||||
}
|
||||
|
||||
byte[] qrCodeData = fingerprint.getScannableFingerprint().getSerialized();
|
||||
String qrCodeString = new String(qrCodeData, Charset.forName("ISO-8859-1"));
|
||||
Bitmap qrCodeBitmap = QrCode.create(qrCodeString);
|
||||
|
||||
qrCode.setImageBitmap(qrCodeBitmap);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCodeSegment(final TextView codeView, String segment) {
|
||||
ValueAnimator valueAnimator = new ValueAnimator();
|
||||
valueAnimator.setObjectValues(0, Integer.parseInt(segment));
|
||||
|
||||
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
int value = (int) animation.getAnimatedValue();
|
||||
codeView.setText(String.format(Locale.getDefault(), "%05d", value));
|
||||
}
|
||||
});
|
||||
|
||||
valueAnimator.setEvaluator(new TypeEvaluator<Integer>() {
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
|
||||
return Math.round(startValue + (endValue - startValue) * fraction);
|
||||
}
|
||||
});
|
||||
|
||||
valueAnimator.setDuration(1000);
|
||||
valueAnimator.start();
|
||||
}
|
||||
|
||||
private String[] getSegments(Fingerprint fingerprint, int segmentCount) {
|
||||
String[] segments = new String[segmentCount];
|
||||
String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
|
||||
int partSize = digits.length() / segmentCount;
|
||||
|
||||
for (int i=0;i<segmentCount;i++) {
|
||||
segments[i] = digits.substring(i * partSize, (i * partSize) + partSize);
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Bitmap check = BitmapFactory.decodeResource(getResources(), id);
|
||||
float offset = (width - check.getWidth()) / 2;
|
||||
|
||||
canvas.drawBitmap(check, offset, offset, null);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private void animateVerifiedSuccess() {
|
||||
Bitmap qrBitmap = ((BitmapDrawable)qrCode.getDrawable()).getBitmap();
|
||||
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_check_white_48dp);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void animateVerifiedFailure() {
|
||||
Bitmap qrBitmap = ((BitmapDrawable)qrCode.getDrawable()).getBitmap();
|
||||
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_close_white_48dp);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void animateVerified() {
|
||||
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
scaleAnimation.setInterpolator(new FastOutSlowInInterpolator());
|
||||
scaleAnimation.setDuration(800);
|
||||
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
qrVerified.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ScaleAnimation scaleAnimation = new ScaleAnimation(1, 0, 1, 0,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
|
||||
ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE);
|
||||
ViewUtil.animateIn(qrVerified, scaleAnimation);
|
||||
qrCodeContainer.setEnabled(false);
|
||||
}
|
||||
|
||||
private void updateVerifyButton(boolean verified, boolean update) {
|
||||
currentVerifiedState = verified;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override public void onScrollChanged() {
|
||||
if (scrollView.canScrollVertically(-1)) {
|
||||
if (toolbarShadow.getVisibility() != View.VISIBLE) {
|
||||
ViewUtil.fadeIn(toolbarShadow, 250);
|
||||
}
|
||||
} else {
|
||||
if (toolbarShadow.getVisibility() != View.GONE) {
|
||||
ViewUtil.fadeOut(toolbarShadow, 250);
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollView.canScrollVertically(1)) {
|
||||
if (bottomShadow.getVisibility() != View.VISIBLE) {
|
||||
ViewUtil.fadeIn(bottomShadow, 250);
|
||||
}
|
||||
} else {
|
||||
ViewUtil.fadeOut(bottomShadow, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
this.scanningThread = new ScanningThread();
|
||||
this.scanningThread.setScanListener(scanListener);
|
||||
this.scanningThread.setCharacterSet("ISO-8859-1");
|
||||
this.cameraView.onResume();
|
||||
this.cameraView.setPreviewCallback(scanningThread);
|
||||
this.scanningThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
this.cameraView.onPause();
|
||||
this.scanningThread.stopScanning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
this.cameraView.onPause();
|
||||
this.cameraView.onResume();
|
||||
this.cameraView.setPreviewCallback(scanningThread);
|
||||
}
|
||||
|
||||
public void setScanListener(ScanListener listener) {
|
||||
if (this.scanningThread != null) scanningThread.setScanListener(listener);
|
||||
this.scanListener = listener;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
private void processIntent(@NonNull Intent intent) {
|
||||
if (ANSWER_ACTION.equals(intent.getAction())) {
|
||||
viewModel.setRecipient(EventBus.getDefault().getStickyEvent(WebRtcViewModel.class).getRecipient());
|
||||
handleAnswerWithAudio();
|
||||
} else if (DENY_ACTION.equals(intent.getAction())) {
|
||||
handleDenyCall();
|
||||
@@ -403,24 +402,19 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void handleAnswerWithAudio() {
|
||||
Recipient recipient = viewModel.getRecipient().get();
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone),
|
||||
R.drawable.ic_mic_solid_24)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
|
||||
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.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)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(false);
|
||||
})
|
||||
.onAnyDenied(this::handleDenyCall)
|
||||
.execute();
|
||||
}
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(false);
|
||||
})
|
||||
.onAnyDenied(this::handleDenyCall)
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void handleAnswerWithVideo() {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -17,8 +16,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class AudioCodec {
|
||||
public class AudioCodec implements Recorder {
|
||||
|
||||
private static final String TAG = Log.tag(AudioCodec.class);
|
||||
|
||||
@@ -51,12 +49,19 @@ public class AudioCodec {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ParcelFileDescriptor fileDescriptor) {
|
||||
Log.i(TAG, "Recording voice note using AudioCodec.");
|
||||
start(new ParcelFileDescriptor.AutoCloseOutputStream(fileDescriptor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
running = false;
|
||||
while (!finished) Util.wait(this, 0);
|
||||
}
|
||||
|
||||
public void start(final OutputStream outputStream) {
|
||||
private void start(final OutputStream outputStream) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@@ -13,6 +12,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
@@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class AudioRecorder {
|
||||
|
||||
private static final String TAG = Log.tag(AudioRecorder.class);
|
||||
@@ -29,8 +28,8 @@ public class AudioRecorder {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private AudioCodec audioCodec;
|
||||
private Uri captureUri;
|
||||
private Recorder recorder;
|
||||
private Uri captureUri;
|
||||
|
||||
public AudioRecorder(@NonNull Context context) {
|
||||
this.context = context;
|
||||
@@ -42,7 +41,7 @@ public class AudioRecorder {
|
||||
executor.execute(() -> {
|
||||
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
|
||||
try {
|
||||
if (audioCodec != null) {
|
||||
if (recorder != null) {
|
||||
throw new AssertionError("We can only record once at a time.");
|
||||
}
|
||||
|
||||
@@ -52,9 +51,9 @@ public class AudioRecorder {
|
||||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||
.withMimeType(MediaUtil.AUDIO_AAC)
|
||||
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
||||
audioCodec = new AudioCodec();
|
||||
|
||||
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
|
||||
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
|
||||
recorder.start(fds[1]);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
@@ -67,12 +66,12 @@ public class AudioRecorder {
|
||||
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
|
||||
|
||||
executor.execute(() -> {
|
||||
if (audioCodec == null) {
|
||||
if (recorder == null) {
|
||||
sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!"));
|
||||
return;
|
||||
}
|
||||
|
||||
audioCodec.stop();
|
||||
recorder.stop();
|
||||
|
||||
try {
|
||||
long size = MediaUtil.getMediaSize(context, captureUri);
|
||||
@@ -82,7 +81,7 @@ public class AudioRecorder {
|
||||
sendToFuture(future, ioe);
|
||||
}
|
||||
|
||||
audioCodec = null;
|
||||
recorder = null;
|
||||
captureUri = null;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Wrap Android's {@link MediaRecorder} for use with voice notes.
|
||||
*/
|
||||
public class MediaRecorderWrapper implements Recorder {
|
||||
|
||||
private static final String TAG = Log.tag(MediaRecorderWrapper.class);
|
||||
|
||||
private static final int SAMPLE_RATE = 44100;
|
||||
private static final int CHANNELS = 1;
|
||||
private static final int BIT_RATE = 32000;
|
||||
|
||||
private MediaRecorder recorder = null;
|
||||
|
||||
@Override
|
||||
public void start(ParcelFileDescriptor fileDescriptor) throws IOException {
|
||||
Log.i(TAG, "Recording voice note using MediaRecorderWrapper.");
|
||||
recorder = new MediaRecorder();
|
||||
|
||||
try {
|
||||
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
||||
recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
|
||||
recorder.setOutputFile(fileDescriptor.getFileDescriptor());
|
||||
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
||||
recorder.setAudioSamplingRate(SAMPLE_RATE);
|
||||
recorder.setAudioEncodingBitRate(BIT_RATE);
|
||||
recorder.setAudioChannels(CHANNELS);
|
||||
recorder.prepare();
|
||||
recorder.start();
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(TAG, "Unable to start recording", e);
|
||||
recorder.release();
|
||||
recorder = null;
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (recorder == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
recorder.stop();
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getClass() != RuntimeException.class) {
|
||||
throw e;
|
||||
} else {
|
||||
Log.d(TAG, "Recording stopped with no data captured.");
|
||||
}
|
||||
} finally {
|
||||
recorder.release();
|
||||
recorder = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Simple abstraction of the interface for the original voice note recording and the new.
|
||||
*/
|
||||
public interface Recorder {
|
||||
void start(ParcelFileDescriptor fileDescriptor) throws IOException;
|
||||
void stop();
|
||||
}
|
||||
@@ -4,9 +4,10 @@ import android.view.View
|
||||
import android.widget.ImageView
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
typealias OnAvatarColorClickListener = (Avatars.ColorPair) -> Unit
|
||||
|
||||
@@ -20,7 +21,7 @@ data class AvatarColorItem(
|
||||
|
||||
companion object {
|
||||
fun registerViewHolder(adapter: MappingAdapter, onAvatarColorClickListener: OnAvatarColorClickListener) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onAvatarColorClickListener) }, R.layout.avatar_color_item))
|
||||
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onAvatarColorClickListener) }, R.layout.avatar_color_item))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,11 +143,10 @@ object Avatars {
|
||||
)
|
||||
|
||||
data class ColorPair(
|
||||
val backgroundAvatarColor: AvatarColor,
|
||||
val foregroundAvatarColor: ForegroundColor
|
||||
@ColorInt val backgroundColor: Int,
|
||||
@ColorInt val foregroundColor: Int,
|
||||
val code: String
|
||||
) {
|
||||
@ColorInt val backgroundColor: Int = backgroundAvatarColor.colorInt()
|
||||
@ColorInt val foregroundColor: Int = foregroundAvatarColor.colorInt
|
||||
val code: String = backgroundAvatarColor.serialize()
|
||||
constructor(backgroundAvatarColor: AvatarColor, foregroundAvatarColor: ForegroundColor) : this(backgroundAvatarColor.colorInt(), foregroundAvatarColor.colorInt, backgroundAvatarColor.serialize())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,13 @@ class TextAvatarDrawable(
|
||||
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
|
||||
val width = bounds.width()
|
||||
val candidates = EmojiProvider.getCandidates(avatar.text)
|
||||
var hasEmoji = false
|
||||
|
||||
textPaint.textSize = textSize
|
||||
|
||||
val newText = if (candidates == null || candidates.size() == 0) {
|
||||
SpannableString(avatar.text)
|
||||
} else {
|
||||
EmojiProvider.emojify(context, candidates, avatar.text, textPaint, synchronous)
|
||||
EmojiProvider.emojify(context, candidates, avatar.text, textPaint, synchronous, true)
|
||||
}
|
||||
|
||||
if (newText == null) return
|
||||
|
||||
@@ -28,8 +28,9 @@ import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
@@ -198,18 +199,18 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
|
||||
private fun openPhotoEditor(photo: Avatar.Photo) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
|
||||
.safeNavigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
|
||||
}
|
||||
|
||||
private fun openVectorEditor(vector: Avatar.Vector) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
|
||||
.safeNavigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
|
||||
}
|
||||
|
||||
private fun openTextEditor(text: Avatar.Text?) {
|
||||
val bundle = if (text != null) AvatarBundler.bundleText(text) else null
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
|
||||
.safeNavigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -243,4 +244,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
}
|
||||
.execute()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
typealias OnAvatarClickListener = (Avatar, Boolean) -> Unit
|
||||
@@ -27,7 +28,7 @@ object AvatarPickerItem {
|
||||
private val SELECTION_CHANGED = Any()
|
||||
|
||||
fun register(adapter: MappingAdapter, onAvatarClickListener: OnAvatarClickListener, onAvatarLongClickListener: OnAvatarLongClickListener) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onAvatarClickListener, onAvatarLongClickListener) }, R.layout.avatar_picker_item))
|
||||
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onAvatarClickListener, onAvatarLongClickListener) }, R.layout.avatar_picker_item))
|
||||
}
|
||||
|
||||
class Model(val avatar: Avatar, val isSelected: Boolean) : MappingModel<Model> {
|
||||
|
||||
@@ -27,8 +27,8 @@ 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
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
||||
/**
|
||||
* Fragment to create an avatar based off of a Vector or Text (via a pager)
|
||||
|
||||
@@ -15,8 +15,8 @@ import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
import org.thoughtcrime.securesms.avatar.AvatarColorItem
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
||||
/**
|
||||
* Fragment to create an avatar based off a default vector.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.thoughtcrime.securesms.backup
|
||||
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
|
||||
/**
|
||||
* Queries used by backup exporter to estimate total counts for various complicated tables.
|
||||
*/
|
||||
object BackupCountQueries {
|
||||
|
||||
const val mmsCount: String = "SELECT COUNT(*) FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.VIEW_ONCE} <= 0"
|
||||
|
||||
const val smsCount: String = "SELECT COUNT(*) FROM ${SmsDatabase.TABLE_NAME} WHERE ${SmsDatabase.EXPIRES_IN} <= 0"
|
||||
|
||||
@get:JvmStatic
|
||||
val groupReceiptCount: String = """
|
||||
SELECT COUNT(*) FROM ${GroupReceiptDatabase.TABLE_NAME}
|
||||
INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${GroupReceiptDatabase.TABLE_NAME}.${GroupReceiptDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID}
|
||||
WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0
|
||||
""".trimIndent()
|
||||
|
||||
@get:JvmStatic
|
||||
val attachmentCount: String = """
|
||||
SELECT COUNT(*) FROM ${AttachmentDatabase.TABLE_NAME}
|
||||
INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${AttachmentDatabase.TABLE_NAME}.${AttachmentDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID}
|
||||
WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0
|
||||
""".trimIndent()
|
||||
}
|
||||
@@ -13,13 +13,12 @@ import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public abstract class FullBackupBase {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(FullBackupBase.class);
|
||||
private static final int DIGEST_ROUNDS = 250_000;
|
||||
|
||||
static class BackupStream {
|
||||
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
|
||||
try {
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0));
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
byte[] input = passphrase.replace(" ", "").getBytes();
|
||||
@@ -27,8 +26,8 @@ public abstract class FullBackupBase {
|
||||
|
||||
if (salt != null) digest.update(salt);
|
||||
|
||||
for (int i=0;i<250000;i++) {
|
||||
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
|
||||
for (int i = 0; i < DIGEST_ROUNDS; i++) {
|
||||
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0));
|
||||
digest.update(hash);
|
||||
hash = digest.digest(input);
|
||||
}
|
||||
@@ -47,20 +46,34 @@ public abstract class FullBackupBase {
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
private final int count;
|
||||
private final long count;
|
||||
private final long estimatedTotalCount;
|
||||
|
||||
BackupEvent(Type type, int count) {
|
||||
this.type = type;
|
||||
this.count = count;
|
||||
BackupEvent(Type type, long count, long estimatedTotalCount) {
|
||||
this.type = type;
|
||||
this.count = count;
|
||||
this.estimatedTotalCount = estimatedTotalCount;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
public long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public long getEstimatedTotalCount() {
|
||||
return estimatedTotalCount;
|
||||
}
|
||||
|
||||
public double getCompletionPercentage() {
|
||||
if (estimatedTotalCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.min(99.9f, (double) count * 100L / (double) estimatedTotalCount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,10 +27,12 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.database.MentionDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.ReactionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
|
||||
@@ -39,10 +41,13 @@ 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.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -75,6 +80,11 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
private static final String TAG = Log.tag(FullBackupExporter.class);
|
||||
|
||||
private static final long DATABASE_VERSION_RECORD_COUNT = 1L;
|
||||
private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L;
|
||||
private static final long IDENTITY_KEY_BACKUP_RECORD_COUNT = 2L;
|
||||
private static final long FINAL_MESSAGE_COUNT = 1L;
|
||||
|
||||
private static final Set<String> BLACKLISTED_TABLES = SetUtil.newHashSet(
|
||||
SignedPreKeyDatabase.TABLE_NAME,
|
||||
OneTimePreKeyDatabase.TABLE_NAME,
|
||||
@@ -134,58 +144,66 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
@NonNull BackupCancellationSignal cancellationSignal)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
|
||||
int count = 0;
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
|
||||
int count = 0;
|
||||
long estimatedCountOutside = 0L;
|
||||
|
||||
try {
|
||||
outputStream.writeDatabaseVersion(input.getVersion());
|
||||
count++;
|
||||
|
||||
List<String> tables = exportSchema(input, outputStream);
|
||||
count += tables.size() * 3;
|
||||
count += tables.size() * TABLE_RECORD_COUNT_MULTIPLIER;
|
||||
|
||||
final long estimatedCount = calculateCount(context, input, tables);
|
||||
estimatedCountOutside = estimatedCount;
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch("Backup");
|
||||
|
||||
for (String table : tables) {
|
||||
throwIfCanceled(cancellationSignal);
|
||||
if (table.equals(MmsDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, cancellationSignal);
|
||||
count = exportTable(table, input, outputStream, cursor -> isNonExpiringMmsMessage(cursor) && isNotReleaseChannel(cursor), null, count, estimatedCount, cancellationSignal);
|
||||
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, cancellationSignal);
|
||||
count = exportTable(table, input, outputStream, cursor -> isNonExpiringSmsMessage(cursor) && isNotReleaseChannel(cursor), null, count, estimatedCount, cancellationSignal);
|
||||
} else if (table.equals(ReactionDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, new MessageId(CursorUtil.requireLong(cursor, ReactionDatabase.MESSAGE_ID), CursorUtil.requireBoolean(cursor, ReactionDatabase.IS_MMS))), null, count, estimatedCount, cancellationSignal);
|
||||
} else if (table.equals(MentionDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, CursorUtil.requireLong(cursor, MentionDatabase.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
|
||||
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, cancellationSignal);
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, estimatedCount, cancellationSignal);
|
||||
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount), count, cancellationSignal);
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
|
||||
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount), count, cancellationSignal);
|
||||
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
|
||||
} else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) {
|
||||
count = exportTable(table, input, outputStream, null, null, count, cancellationSignal);
|
||||
count = exportTable(table, input, outputStream, null, null, count, estimatedCount, cancellationSignal);
|
||||
}
|
||||
stopwatch.split("table::" + table);
|
||||
}
|
||||
|
||||
for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) {
|
||||
throwIfCanceled(cancellationSignal);
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
outputStream.write(preference);
|
||||
}
|
||||
|
||||
|
||||
for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) {
|
||||
throwIfCanceled(cancellationSignal);
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
outputStream.write(preference);
|
||||
}
|
||||
|
||||
stopwatch.split("prefs");
|
||||
|
||||
count = exportKeyValues(outputStream, SignalStore.getKeysToIncludeInBackup(), count, cancellationSignal);
|
||||
count = exportKeyValues(outputStream, SignalStore.getKeysToIncludeInBackup(), count, estimatedCount, cancellationSignal);
|
||||
|
||||
stopwatch.split("key_values");
|
||||
|
||||
for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) {
|
||||
throwIfCanceled(cancellationSignal);
|
||||
if (avatar != null) {
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength());
|
||||
}
|
||||
}
|
||||
@@ -198,7 +216,49 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
if (closeOutputStream) {
|
||||
outputStream.close();
|
||||
}
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside));
|
||||
}
|
||||
}
|
||||
|
||||
private static long calculateCount(@NonNull Context context, @NonNull SQLiteDatabase input, List<String> tables) {
|
||||
long count = DATABASE_VERSION_RECORD_COUNT + TABLE_RECORD_COUNT_MULTIPLIER * tables.size();
|
||||
|
||||
for (String table : tables) {
|
||||
if (table.equals(MmsDatabase.TABLE_NAME)) {
|
||||
count += getCount(input, BackupCountQueries.mmsCount);
|
||||
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
|
||||
count += getCount(input, BackupCountQueries.smsCount);
|
||||
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
|
||||
count += getCount(input, BackupCountQueries.getGroupReceiptCount());
|
||||
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
|
||||
count += getCount(input, BackupCountQueries.getAttachmentCount());
|
||||
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
|
||||
count += getCount(input, "SELECT COUNT(*) FROM " + table);
|
||||
} else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) {
|
||||
count += getCount(input, "SELECT COUNT(*) FROM " + table);
|
||||
}
|
||||
}
|
||||
|
||||
count += IDENTITY_KEY_BACKUP_RECORD_COUNT;
|
||||
|
||||
count += TextSecurePreferences.getPreferencesToSaveToBackupCount(context);
|
||||
|
||||
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())
|
||||
.getDataSet();
|
||||
for (String key : SignalStore.getKeysToIncludeInBackup()) {
|
||||
if (dataSet.containsKey(key)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
count += AvatarHelper.getAvatarCount(context);
|
||||
|
||||
return count + FINAL_MESSAGE_COUNT;
|
||||
}
|
||||
|
||||
private static long getCount(@NonNull SQLiteDatabase input, @NonNull String query) {
|
||||
try (Cursor cursor = input.rawQuery(query)) {
|
||||
return cursor.moveToFirst() ? cursor.getLong(0) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +304,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
@Nullable Predicate<Cursor> predicate,
|
||||
@Nullable PostProcessor postProcess,
|
||||
int count,
|
||||
long estimatedCount,
|
||||
@NonNull BackupCancellationSignal cancellationSignal)
|
||||
throws IOException
|
||||
{
|
||||
@@ -283,7 +344,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
statement.append(')');
|
||||
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
outputStream.write(statementBuilder.setStatement(statement.toString()).build());
|
||||
|
||||
if (postProcess != null) {
|
||||
@@ -296,7 +357,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count) {
|
||||
private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) {
|
||||
try {
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
|
||||
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
|
||||
@@ -321,7 +382,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
|
||||
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
|
||||
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -331,7 +392,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count) {
|
||||
private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) {
|
||||
try {
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
|
||||
@@ -340,7 +401,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
|
||||
|
||||
if (!TextUtils.isEmpty(data) && size > 0) {
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
|
||||
outputStream.writeSticker(rowId, inputStream, size);
|
||||
}
|
||||
@@ -371,6 +432,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream,
|
||||
@NonNull List<String> keysToIncludeInBackup,
|
||||
int count,
|
||||
long estimatedCount,
|
||||
BackupCancellationSignal cancellationSignal) throws IOException
|
||||
{
|
||||
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())
|
||||
@@ -401,7 +463,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
throw new AssertionError("Unknown type: " + type);
|
||||
}
|
||||
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
outputStream.write(builder.build());
|
||||
}
|
||||
|
||||
@@ -417,21 +479,46 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0;
|
||||
}
|
||||
|
||||
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
|
||||
String[] columns = new String[] { MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
|
||||
String where = MmsDatabase.ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(mmsId) };
|
||||
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull MessageId messageId) {
|
||||
if (messageId.isMms()) {
|
||||
return isForNonExpiringMmsMessageAndNotReleaseChannel(db, messageId.getId());
|
||||
} else {
|
||||
return isForNonExpiringSmsMessage(db, messageId.getId());
|
||||
}
|
||||
}
|
||||
|
||||
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
|
||||
if (mmsCursor != null && mmsCursor.moveToFirst()) {
|
||||
return mmsCursor.getLong(mmsCursor.getColumnIndexOrThrow(MmsDatabase.EXPIRES_IN)) == 0 &&
|
||||
mmsCursor.getLong(mmsCursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 0;
|
||||
private static boolean isForNonExpiringSmsMessage(@NonNull SQLiteDatabase db, long smsId) {
|
||||
String[] columns = new String[] { SmsDatabase.EXPIRES_IN };
|
||||
String where = SmsDatabase.ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(smsId) };
|
||||
|
||||
try (Cursor cursor = db.query(SmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return isNonExpiringSmsMessage(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isForNonExpiringMmsMessageAndNotReleaseChannel(@NonNull SQLiteDatabase db, long mmsId) {
|
||||
String[] columns = new String[] { MmsDatabase.RECIPIENT_ID, MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
|
||||
String where = MmsDatabase.ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(mmsId) };
|
||||
|
||||
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
|
||||
if (mmsCursor != null && mmsCursor.moveToFirst()) {
|
||||
return isNonExpiringMmsMessage(mmsCursor) && isNotReleaseChannel(mmsCursor);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isNotReleaseChannel(Cursor cursor) {
|
||||
RecipientId releaseChannel = SignalStore.releaseChannelValues().getReleaseChannelRecipientId();
|
||||
return releaseChannel == null || cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID)) != releaseChannel.toLong();
|
||||
}
|
||||
|
||||
private static class BackupFrameOutputStream extends BackupStream {
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
BackupFrame frame;
|
||||
|
||||
while (!(frame = inputStream.readFrame()).getEnd()) {
|
||||
if (count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count));
|
||||
if (count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count, 0));
|
||||
count++;
|
||||
|
||||
if (frame.hasVersion()) processVersion(db, frame.getVersion());
|
||||
@@ -115,7 +115,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
keyValueDatabase.endTransaction();
|
||||
}
|
||||
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count, 0));
|
||||
}
|
||||
|
||||
private static @NonNull InputStream getInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException{
|
||||
@@ -207,7 +207,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
|
||||
if (avatar.hasRecipientId()) {
|
||||
RecipientId recipientId = RecipientId.from(avatar.getRecipientId());
|
||||
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId), avatar.getLength());
|
||||
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId, false), avatar.getLength());
|
||||
} else {
|
||||
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) {
|
||||
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");
|
||||
|
||||
@@ -14,9 +14,10 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import java.security.MessageDigest
|
||||
|
||||
typealias OnBadgeClicked = (Badge, Boolean, Boolean) -> Unit
|
||||
@@ -165,8 +166,8 @@ data class Badge(
|
||||
private val SELECTION_CHANGED = Any()
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onBadgeClicked) }, R.layout.badge_preference_view))
|
||||
mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.badge_preference_view))
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onBadgeClicked) }, R.layout.badge_preference_view))
|
||||
mappingAdapter.registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.badge_preference_view))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,15 @@ import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
object BadgePreview {
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
|
||||
mappingAdapter.registerFactory(SubscriptionModel::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.subscription_flow_badge_preview_preference))
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
|
||||
mappingAdapter.registerFactory(SubscriptionModel::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.subscription_flow_badge_preview_preference))
|
||||
}
|
||||
|
||||
abstract class BadgeModel<T : BadgeModel<T>> : PreferenceModel<T>() {
|
||||
|
||||
@@ -4,8 +4,9 @@ import android.view.View
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
object ExpiredBadge {
|
||||
|
||||
@@ -29,6 +30,6 @@ object ExpiredBadge {
|
||||
}
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.expired_badge_preference))
|
||||
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.expired_badge_preference))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
data class LargeBadge(
|
||||
val badge: Badge
|
||||
@@ -51,8 +52,8 @@ data class LargeBadge(
|
||||
|
||||
companion object {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
|
||||
mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
|
||||
mappingAdapter.registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.badges.self.expired
|
||||
|
||||
import androidx.core.content.ContextCompat
|
||||
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.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.SplashImage
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
|
||||
class CantProcessSubscriptionPaymentBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() {
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
SplashImage.register(adapter)
|
||||
adapter.submitList(getConfiguration().toMappingModelList())
|
||||
}
|
||||
|
||||
private fun getConfiguration(): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(SplashImage.Model(R.drawable.ic_card_process))
|
||||
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__cant_process_subscription_payment, DSLSettingsText.CenterModifier)
|
||||
)
|
||||
|
||||
textPref(
|
||||
summary = DSLSettingsText.from(
|
||||
requireContext().getString(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__were_having_trouble),
|
||||
DSLSettingsText.LearnMoreModifier(ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)) {
|
||||
CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.donation_decline_code_error_url))
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(android.R.string.ok)
|
||||
) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__dont_show_this_again)
|
||||
) {
|
||||
SignalStore.donationsValues().showCantProcessDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
@@ -27,9 +28,13 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
}
|
||||
|
||||
private fun getConfiguration(): DSLConfiguration {
|
||||
val badge: Badge = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments()).badge
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
|
||||
val badge: Badge = args.badge
|
||||
val cancellationReason: UnexpectedSubscriptionCancellation? = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason)
|
||||
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
|
||||
|
||||
val inactive = cancellationReason == UnexpectedSubscriptionCancellation.INACTIVE
|
||||
|
||||
return configure {
|
||||
customPref(ExpiredBadge.Model(badge))
|
||||
|
||||
@@ -50,8 +55,10 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
DSLSettingsText.from(
|
||||
if (badge.isBoost()) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired)
|
||||
} else if (inactive) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer_subscription_was_automatically, badge.name)
|
||||
} else {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer, badge.name)
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer_subscription_was_canceled)
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
@@ -109,8 +116,8 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun show(badge: Badge, fragmentManager: FragmentManager) {
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge).build()
|
||||
fun show(badge: Badge, cancellationReason: UnexpectedSubscriptionCancellation?, fragmentManager: FragmentManager) {
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge, cancellationReason?.status).build()
|
||||
val fragment = ExpiredBadgeBottomSheetDialogFragment()
|
||||
fragment.arguments = args.toBundle()
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Fragment to allow user to manage options related to the badges they've unlocked.
|
||||
@@ -37,7 +38,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, _, isFaded ->
|
||||
if (badge.isExpired() || isFaded) {
|
||||
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge))
|
||||
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null))
|
||||
} else {
|
||||
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
|
||||
}
|
||||
@@ -83,7 +84,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
|
||||
onClick = {
|
||||
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
|
||||
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@@ -18,6 +17,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||
setText(recipient, recipient.getDisplayName(getContext()), read, suffix);
|
||||
setText(recipient, recipient.getDisplayNameOrUsername(getContext()), read, suffix);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
|
||||
@@ -62,11 +62,19 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
builder.append(suffix);
|
||||
}
|
||||
|
||||
if (recipient.isReleaseNotes()) {
|
||||
Drawable official = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_20);
|
||||
official.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20));
|
||||
|
||||
builder.append(" ")
|
||||
.append(SpanUtil.buildCenteredImageSpan(official));
|
||||
}
|
||||
|
||||
setText(builder);
|
||||
|
||||
if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
|
||||
else if (recipient.isMuted()) setCompoundDrawablesRelativeWithIntrinsicBounds(getMuted(), null, null, null);
|
||||
else setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
else setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private Drawable getMuted() {
|
||||
|
||||
@@ -96,6 +96,7 @@ public class InputPanel extends LinearLayout
|
||||
private boolean hideForGroupState;
|
||||
private boolean hideForBlockedState;
|
||||
private boolean hideForSearch;
|
||||
private boolean hideForSelection;
|
||||
|
||||
private ConversationStickerSuggestionAdapter stickerSuggestionAdapter;
|
||||
|
||||
@@ -336,6 +337,11 @@ public class InputPanel extends LinearLayout
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
public void setHideForSelection(boolean hideForSelection) {
|
||||
this.hideForSelection = hideForSelection;
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordPermissionRequired() {
|
||||
if (listener != null) listener.onRecorderPermissionRequired();
|
||||
@@ -515,7 +521,7 @@ public class InputPanel extends LinearLayout
|
||||
}
|
||||
|
||||
private void updateVisibility() {
|
||||
if (hideForGroupState || hideForBlockedState || hideForSearch) {
|
||||
if (hideForGroupState || hideForBlockedState || hideForSearch || hideForSelection) {
|
||||
setVisibility(GONE);
|
||||
} else {
|
||||
setVisibility(VISIBLE);
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
|
||||
/**
|
||||
@@ -121,7 +120,11 @@ public final class RotatableGradientDrawable extends Drawable {
|
||||
public void draw(Canvas canvas) {
|
||||
int save = canvas.save();
|
||||
canvas.rotate(degrees, getBounds().width() / 2f, getBounds().height() / 2f);
|
||||
canvas.drawRect(fillRect, fillPaint);
|
||||
|
||||
int height = fillRect.height();
|
||||
int width = fillRect.width();
|
||||
canvas.drawRect(fillRect.left - width, fillRect.top - height, fillRect.right + width, fillRect.bottom + height, fillPaint);
|
||||
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class SearchView extends androidx.appcompat.widget.SearchView {
|
||||
result = new InputFilter[1];
|
||||
}
|
||||
|
||||
result[0] = new EmojiFilter(view);
|
||||
result[0] = new EmojiFilter(view, false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
@@ -66,9 +67,6 @@ public class ZoomingImageView extends FrameLayout {
|
||||
this.photoView = findViewById(R.id.image_view);
|
||||
this.subsamplingImageView = findViewById(R.id.subsampling_image_view);
|
||||
|
||||
this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
|
||||
this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
|
||||
|
||||
this.photoView.setZoomTransitionDuration(ZOOM_TRANSITION_DURATION);
|
||||
this.photoView.setScaleLevels(ZOOM_LEVEL_MIN, SMALL_IMAGES_ZOOM_LEVEL_MID, SMALL_IMAGES_ZOOM_LEVEL_MAX);
|
||||
|
||||
@@ -129,6 +127,26 @@ public class ZoomingImageView extends FrameLayout {
|
||||
subsamplingImageView.setVisibility(View.VISIBLE);
|
||||
photoView.setVisibility(View.GONE);
|
||||
|
||||
// We manually set the orientation ourselves because using
|
||||
// SubsamplingScaleImageView.ORIENTATION_USE_EXIF is unreliable:
|
||||
// https://github.com/signalapp/Signal-Android/issues/11732#issuecomment-963203545
|
||||
try {
|
||||
final InputStream inputStream = PartAuthority.getAttachmentStream(getContext(), uri);
|
||||
final int orientation = BitmapUtil.getExifOrientation(new ExifInterface(inputStream));
|
||||
inputStream.close();
|
||||
if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
|
||||
subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_90);
|
||||
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
|
||||
subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_180);
|
||||
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
|
||||
subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_270);
|
||||
} else {
|
||||
subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_0);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
subsamplingImageView.setImage(ImageSource.uri(uri));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Emoji {
|
||||
|
||||
private final List<String> variations;
|
||||
private final List<String> rawVariations;
|
||||
|
||||
public Emoji(String... variations) {
|
||||
this.variations = Arrays.asList(variations);
|
||||
this(Arrays.asList(variations), Collections.emptyList());
|
||||
}
|
||||
|
||||
public Emoji(List<String> variations) {
|
||||
this(variations, Collections.emptyList());
|
||||
}
|
||||
|
||||
public Emoji(List<String> variations, List<String> rawVariations) {
|
||||
this.variations = variations;
|
||||
this.rawVariations = rawVariations;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
@@ -26,4 +35,11 @@ public class Emoji {
|
||||
public boolean hasMultipleVariations() {
|
||||
return variations.size() > 1;
|
||||
}
|
||||
|
||||
public @Nullable String getRawVariation(int variationIndex) {
|
||||
if (rawVariations != null && variationIndex >= 0 && variationIndex < rawVariations.size()) {
|
||||
return rawVariations.get(variationIndex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@ public class EmojiEditText extends AppCompatEditText {
|
||||
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
||||
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||
boolean jumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
|
||||
a.recycle();
|
||||
|
||||
if (!isInEditMode() && (forceCustom || !SignalStore.settings().isPreferSystemEmoji())) {
|
||||
setFilters(appendEmojiFilter(this.getFilters()));
|
||||
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ public class EmojiEditText extends AppCompatEditText {
|
||||
else super.invalidateDrawable(drawable);
|
||||
}
|
||||
|
||||
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters) {
|
||||
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
|
||||
InputFilter[] result;
|
||||
|
||||
if (originalFilters != null) {
|
||||
@@ -64,7 +65,7 @@ public class EmojiEditText extends AppCompatEditText {
|
||||
result = new InputFilter[1];
|
||||
}
|
||||
|
||||
result[0] = new EmojiFilter(this);
|
||||
result[0] = new EmojiFilter(this, jumboEmoji);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ import android.widget.TextView;
|
||||
|
||||
public class EmojiFilter implements InputFilter {
|
||||
private TextView view;
|
||||
private boolean jumboEmoji;
|
||||
|
||||
public EmojiFilter(TextView view) {
|
||||
this.view = view;
|
||||
public EmojiFilter(TextView view, boolean jumboEmoji) {
|
||||
this.view = view;
|
||||
this.jumboEmoji = jumboEmoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -19,7 +21,7 @@ public class EmojiFilter implements InputFilter {
|
||||
char[] v = new char[end - start];
|
||||
TextUtils.getChars(source, start, end, v, 0);
|
||||
|
||||
Spannable emojified = EmojiProvider.emojify(new String(v), view);
|
||||
Spannable emojified = EmojiProvider.emojify(new String(v), view, jumboEmoji);
|
||||
|
||||
if (source instanceof Spanned && emojified != null) {
|
||||
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.Emoj
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -10,9 +10,10 @@ import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
|
||||
|
||||
public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWindow.OnDismissListener {
|
||||
|
||||
|
||||
@@ -19,15 +19,17 @@ import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiFiles;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiPageCache;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class EmojiProvider {
|
||||
|
||||
@@ -39,23 +41,24 @@ public class EmojiProvider {
|
||||
return new EmojiParser(EmojiSource.getLatest().getEmojiTree()).findCandidates(text);
|
||||
}
|
||||
|
||||
static @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) {
|
||||
static @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv, boolean jumboEmoji) {
|
||||
if (tv.isInEditMode()) {
|
||||
return null;
|
||||
} else {
|
||||
return emojify(getCandidates(text), text, tv);
|
||||
return emojify(getCandidates(text), text, tv, jumboEmoji);
|
||||
}
|
||||
}
|
||||
|
||||
static @Nullable Spannable emojify(@Nullable EmojiParser.CandidateList matches,
|
||||
@Nullable CharSequence text,
|
||||
@NonNull TextView tv)
|
||||
@NonNull TextView tv,
|
||||
boolean jumboEmoji)
|
||||
{
|
||||
if (matches == null || text == null || tv.isInEditMode()) return null;
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||
|
||||
for (EmojiParser.Candidate candidate : matches) {
|
||||
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo(), tv::requestLayout);
|
||||
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo(), tv::requestLayout, jumboEmoji);
|
||||
|
||||
if (drawable != null) {
|
||||
builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(),
|
||||
@@ -70,7 +73,8 @@ public class EmojiProvider {
|
||||
@Nullable EmojiParser.CandidateList matches,
|
||||
@Nullable CharSequence text,
|
||||
@NonNull Paint paint,
|
||||
boolean synchronous)
|
||||
boolean synchronous,
|
||||
boolean jumboEmoji)
|
||||
{
|
||||
if (matches == null || text == null) return null;
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||
@@ -78,9 +82,9 @@ public class EmojiProvider {
|
||||
for (EmojiParser.Candidate candidate : matches) {
|
||||
Drawable drawable;
|
||||
if (synchronous) {
|
||||
drawable = getEmojiDrawableSync(context, candidate.getDrawInfo());
|
||||
drawable = getEmojiDrawableSync(context, candidate.getDrawInfo(), jumboEmoji);
|
||||
} else {
|
||||
drawable = getEmojiDrawable(context, candidate.getDrawInfo(), null);
|
||||
drawable = getEmojiDrawable(context, candidate.getDrawInfo(), null, jumboEmoji);
|
||||
}
|
||||
|
||||
if (drawable != null) {
|
||||
@@ -93,8 +97,12 @@ public class EmojiProvider {
|
||||
}
|
||||
|
||||
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji) {
|
||||
return getEmojiDrawable(context, emoji, false);
|
||||
}
|
||||
|
||||
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji, boolean jumboEmoji) {
|
||||
EmojiDrawInfo drawInfo = EmojiSource.getLatest().getEmojiTree().getEmoji(emoji, 0, emoji.length());
|
||||
return getEmojiDrawable(context, drawInfo, null);
|
||||
return getEmojiDrawable(context, drawInfo, null, jumboEmoji);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +112,7 @@ public class EmojiProvider {
|
||||
* @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) {
|
||||
private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, @Nullable Runnable onEmojiLoaded, boolean jumboEmoji) {
|
||||
if (drawInfo == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -112,6 +120,7 @@ public class EmojiProvider {
|
||||
final int lowMemoryDecodeScale = DeviceProperties.isLowMemoryDevice(context) ? 2 : 1;
|
||||
final EmojiSource source = EmojiSource.getLatest();
|
||||
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
|
||||
final AtomicBoolean jumboLoaded = new AtomicBoolean(false);
|
||||
|
||||
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
|
||||
|
||||
@@ -122,9 +131,11 @@ public class EmojiProvider {
|
||||
@Override
|
||||
public void onSuccess(Bitmap result) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
drawable.setBitmap(result);
|
||||
if (onEmojiLoaded != null) {
|
||||
onEmojiLoaded.run();
|
||||
if (!jumboLoaded.get()) {
|
||||
drawable.setBitmap(result);
|
||||
if (onEmojiLoaded != null) {
|
||||
onEmojiLoaded.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -138,6 +149,40 @@ public class EmojiProvider {
|
||||
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
|
||||
}
|
||||
|
||||
if (jumboEmoji && drawInfo.getJumboSheet() != null) {
|
||||
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo);
|
||||
if (result instanceof JumboEmoji.LoadResult.Immediate) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
jumboLoaded.set(true);
|
||||
drawable.setSingleBitmap(((JumboEmoji.LoadResult.Immediate) result).getBitmap());
|
||||
});
|
||||
} else if (result instanceof JumboEmoji.LoadResult.Async) {
|
||||
((JumboEmoji.LoadResult.Async) result).getTask().addListener(new FutureTaskListener<Bitmap>() {
|
||||
@Override
|
||||
public void onSuccess(Bitmap result) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
jumboLoaded.set(true);
|
||||
drawable.setSingleBitmap(result);
|
||||
if (onEmojiLoaded != null) {
|
||||
onEmojiLoaded.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException exception) {
|
||||
if (exception.getCause() instanceof JumboEmoji.CannotAutoDownload) {
|
||||
Log.i(TAG, "Download restrictions are preventing jumbomoji use");
|
||||
} else {
|
||||
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
@@ -147,7 +192,7 @@ public class EmojiProvider {
|
||||
* @param context Context object used in reading and writing from disk
|
||||
* @param drawInfo Information about the emoji being displayed
|
||||
*/
|
||||
private static @Nullable Drawable getEmojiDrawableSync(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo) {
|
||||
private static @Nullable Drawable getEmojiDrawableSync(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, boolean jumboEmoji) {
|
||||
ThreadUtil.assertNotMainThread();
|
||||
if (drawInfo == null) {
|
||||
return null;
|
||||
@@ -157,24 +202,49 @@ public class EmojiProvider {
|
||||
final EmojiSource source = EmojiSource.getLatest();
|
||||
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
|
||||
|
||||
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
|
||||
Bitmap bitmap = null;
|
||||
Bitmap bitmap = null;
|
||||
|
||||
if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) {
|
||||
Log.d(TAG, "Cached emoji page: " + drawInfo.getPage().getUri().toString());
|
||||
bitmap = ((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap();
|
||||
} else if (loadResult instanceof EmojiPageCache.LoadResult.Async) {
|
||||
Log.d(TAG, "Loading emoji page: " + drawInfo.getPage().getUri().toString());
|
||||
try {
|
||||
bitmap = ((EmojiPageCache.LoadResult.Async) loadResult).getTask().get(2, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
|
||||
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
|
||||
if (jumboEmoji && drawInfo.getJumboSheet() != null) {
|
||||
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo);
|
||||
if (result instanceof JumboEmoji.LoadResult.Immediate) {
|
||||
bitmap = ((JumboEmoji.LoadResult.Immediate) result).getBitmap();
|
||||
} else if (result instanceof JumboEmoji.LoadResult.Async) {
|
||||
try {
|
||||
bitmap = ((JumboEmoji.LoadResult.Async) result).getTask().get(10, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
|
||||
if (exception.getCause() instanceof JumboEmoji.CannotAutoDownload) {
|
||||
Log.i(TAG, "Download restrictions are preventing jumbomoji use");
|
||||
} else {
|
||||
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
drawable.setSingleBitmap(bitmap);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
|
||||
}
|
||||
|
||||
drawable.setBitmap(bitmap);
|
||||
if (!jumboEmoji || bitmap == null) {
|
||||
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
|
||||
|
||||
if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) {
|
||||
Log.d(TAG, "Cached emoji page: " + drawInfo.getPage().getUri().toString());
|
||||
bitmap = ((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap();
|
||||
} else if (loadResult instanceof EmojiPageCache.LoadResult.Async) {
|
||||
Log.d(TAG, "Loading emoji page: " + drawInfo.getPage().getUri().toString());
|
||||
try {
|
||||
bitmap = ((EmojiPageCache.LoadResult.Async) loadResult).getTask().get(2, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
|
||||
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
|
||||
}
|
||||
|
||||
drawable.setBitmap(bitmap);
|
||||
}
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
@@ -183,7 +253,8 @@ public class EmojiProvider {
|
||||
private final float intrinsicHeight;
|
||||
private final Rect emojiBounds;
|
||||
|
||||
private Bitmap bmp;
|
||||
private Bitmap bmp;
|
||||
private boolean isSingleBitmap;
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
@@ -219,12 +290,21 @@ public class EmojiProvider {
|
||||
}
|
||||
|
||||
canvas.drawBitmap(bmp,
|
||||
emojiBounds,
|
||||
isSingleBitmap ? null : emojiBounds,
|
||||
getBounds(),
|
||||
PAINT);
|
||||
}
|
||||
|
||||
public void setBitmap(Bitmap bitmap) {
|
||||
setBitmap(bitmap, false);
|
||||
}
|
||||
|
||||
public void setSingleBitmap(Bitmap bitmap) {
|
||||
setBitmap(bitmap, true);
|
||||
}
|
||||
|
||||
private void setBitmap(Bitmap bitmap, boolean isSingleBitmap) {
|
||||
this.isSingleBitmap = isSingleBitmap;
|
||||
if (bmp == null || !bmp.sameAs(bitmap)) {
|
||||
bmp = bitmap;
|
||||
invalidateSelf();
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Annotation;
|
||||
import android.text.Layout;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -12,6 +13,7 @@ import android.text.TextDirectionHeuristic;
|
||||
import android.text.TextDirectionHeuristics;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.TransformationMethod;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ViewGroup;
|
||||
@@ -21,24 +23,29 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
|
||||
public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
private final boolean scaleEmojis;
|
||||
|
||||
private static final char ELLIPSIS = '…';
|
||||
private static final char ELLIPSIS = '…';
|
||||
private static final float JUMBOMOJI_SCALE = 0.8f;
|
||||
|
||||
private boolean forceCustom;
|
||||
private CharSequence previousText;
|
||||
@@ -55,6 +62,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private int lastLineWidth = -1;
|
||||
private TextDirectionHeuristic textDirection;
|
||||
private boolean isJumbomoji;
|
||||
private boolean forceJumboEmoji;
|
||||
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
|
||||
@@ -75,6 +83,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
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);
|
||||
forceJumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
|
||||
@@ -106,14 +115,13 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
EmojiParser.CandidateList candidates = isInEditMode() ? null : EmojiProvider.getCandidates(text);
|
||||
|
||||
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
||||
if (scaleEmojis && candidates != null && candidates.allEmojis && (candidates.hasJumboForAll() || JumboEmoji.canDownloadJumbo(getContext()))) {
|
||||
int emojis = candidates.size();
|
||||
float scale = 1.0f;
|
||||
|
||||
if (emojis <= 8) scale += 0.25f;
|
||||
if (emojis <= 6) scale += 0.25f;
|
||||
if (emojis <= 4) scale += 0.25f;
|
||||
if (emojis <= 2) scale += 0.25f;
|
||||
if (emojis <= 5) scale += JUMBOMOJI_SCALE;
|
||||
if (emojis <= 4) scale += JUMBOMOJI_SCALE;
|
||||
if (emojis <= 2) scale += JUMBOMOJI_SCALE;
|
||||
|
||||
isJumbomoji = scale > 1.0f;
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize * scale);
|
||||
@@ -135,7 +143,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.SPANNABLE);
|
||||
} else {
|
||||
CharSequence emojified = EmojiProvider.emojify(candidates, text, this);
|
||||
CharSequence emojified = EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji);
|
||||
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
|
||||
}
|
||||
|
||||
@@ -155,6 +163,8 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
|
||||
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
widthMeasureSpec = applyWidthMeasureRoundingFix(widthMeasureSpec);
|
||||
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
CharSequence text = getText();
|
||||
if (getLayout() == null || !measureLastLine || text == null || text.length() == 0) {
|
||||
@@ -175,6 +185,42 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from API 30, there can be a rounding error in text layout when a non-default font
|
||||
* scale is used. This causes a line break to be inserted where there shouldn't be one. Force the
|
||||
* width to be larger to work around this problem.
|
||||
* https://issuetracker.google.com/issues/173574230
|
||||
*
|
||||
* @param widthMeasureSpec the original measure spec passed to {@link #onMeasure(int, int)}
|
||||
* @return the measure spec with the workaround, or the original one.
|
||||
*/
|
||||
private int applyWidthMeasureRoundingFix(int widthMeasureSpec) {
|
||||
if (Build.VERSION.SDK_INT >= 30 && Math.abs(getResources().getConfiguration().fontScale - 1f) > 0.01f) {
|
||||
CharSequence text = getText();
|
||||
if (text != null) {
|
||||
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
|
||||
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
|
||||
float measuredTextWidth = hasMetricAffectingSpan(text) ? Layout.getDesiredWidth(text, getPaint()) : getPaint().measureText(text, 0, text.length());
|
||||
int desiredWidth = (int) measuredTextWidth + getPaddingLeft() + getPaddingRight();
|
||||
|
||||
if (widthSpecMode == MeasureSpec.AT_MOST && desiredWidth < widthSpecSize) {
|
||||
return MeasureSpec.makeMeasureSpec(desiredWidth + 1, MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widthMeasureSpec;
|
||||
}
|
||||
|
||||
private boolean hasMetricAffectingSpan(@NonNull CharSequence text) {
|
||||
if (!(text instanceof Spanned)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((Spanned) text).nextSpanTransition(-1, text.length(), MetricAffectingSpan.class) != text.length();
|
||||
}
|
||||
|
||||
public int getLastLineWidth() {
|
||||
return lastLineWidth;
|
||||
}
|
||||
@@ -221,19 +267,14 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
if (useSystemEmoji || newCandidates == null || newCandidates.size() == 0) {
|
||||
super.setText(newContent, BufferType.SPANNABLE);
|
||||
} else {
|
||||
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this);
|
||||
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
|
||||
super.setText(emojified, BufferType.SPANNABLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ellipsizeEmojiTextForMaxLines() {
|
||||
post(() -> {
|
||||
if (getLayout() == null) {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
return;
|
||||
}
|
||||
|
||||
Runnable ellipsize = () -> {
|
||||
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
|
||||
if (maxLines <= 0 && maxLength < 0) {
|
||||
return;
|
||||
@@ -241,10 +282,10 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
int lineCount = getLineCount();
|
||||
if (lineCount > maxLines) {
|
||||
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
||||
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
|
||||
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
|
||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
|
||||
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
||||
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
|
||||
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
|
||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
|
||||
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, overflowStart))
|
||||
@@ -252,11 +293,20 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
.append(Optional.fromNullable(overflowText).or(""));
|
||||
|
||||
EmojiParser.CandidateList newCandidates = isInEditMode() ? null : EmojiProvider.getCandidates(newContent);
|
||||
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this);
|
||||
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
|
||||
|
||||
super.setText(emojified, BufferType.SPANNABLE);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (getLayout() != null) {
|
||||
ellipsize.run();
|
||||
} else {
|
||||
ViewKt.doOnNextLayout(this, view -> {
|
||||
ellipsize.run();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
|
||||
|
||||
@@ -27,20 +27,20 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||
val endDrawableSize: Int = compoundDrawables[1]?.let { it.intrinsicWidth + compoundDrawablePadding } ?: 0
|
||||
val adjustedWidth: Int = width - startDrawableSize - endDrawableSize
|
||||
|
||||
val newContent = if (width == 0 || maxLines == -1) {
|
||||
val newCandidates = if (isInEditMode) null else EmojiProvider.getCandidates(text)
|
||||
val newText = if (newCandidates == null || newCandidates.size() == 0) {
|
||||
text
|
||||
} else {
|
||||
TextUtils.ellipsize(text, paint, (adjustedWidth * maxLines).toFloat(), TextUtils.TruncateAt.END, false, null)
|
||||
EmojiProvider.emojify(newCandidates, text, this, false)
|
||||
}
|
||||
|
||||
val newCandidates = if (isInEditMode) null else EmojiProvider.getCandidates(newContent)
|
||||
val newText = if (newCandidates == null || newCandidates.size() == 0) {
|
||||
newContent
|
||||
val newContent = if (width == 0 || maxLines == -1) {
|
||||
newText
|
||||
} else {
|
||||
EmojiProvider.emojify(newCandidates, newContent, this)
|
||||
TextUtils.ellipsize(newText, paint, (adjustedWidth * maxLines).toFloat(), TextUtils.TruncateAt.END, false, null)
|
||||
}
|
||||
bufferType = BufferType.SPANNABLE
|
||||
super.setText(newText, type)
|
||||
super.setText(newContent, type)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.emoji.EmojiPage;
|
||||
|
||||
public class EmojiDrawInfo {
|
||||
|
||||
private final EmojiPage page;
|
||||
private final int index;
|
||||
|
||||
public EmojiDrawInfo(final @NonNull EmojiPage page, final int index) {
|
||||
this.page = page;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public @NonNull EmojiPage getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "DrawInfo{" +
|
||||
"page=" + page +
|
||||
", index=" + index +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.emoji.parsing
|
||||
|
||||
import org.thoughtcrime.securesms.emoji.EmojiPage
|
||||
|
||||
data class EmojiDrawInfo(val page: EmojiPage, val index: Int, private val emoji: String, val rawEmoji: String?, val jumboSheet: String?)
|
||||
@@ -24,6 +24,8 @@ package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -127,6 +129,15 @@ public class EmojiParser {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
public boolean hasJumboForAll() {
|
||||
for (Candidate candidate : list) {
|
||||
if (!JumboEmoji.hasJumboEmoji(candidate.drawInfo)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Iterator<Candidate> iterator() {
|
||||
return list.iterator();
|
||||
|
||||
@@ -40,7 +40,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
|
||||
final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities();
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
|
||||
@@ -43,9 +43,9 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.components.menu
|
||||
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
/**
|
||||
* Handles the setup and display of actions shown in a context menu.
|
||||
*/
|
||||
class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
||||
|
||||
private val mappingAdapter = MappingAdapter().apply {
|
||||
registerFactory(DisplayItem::class.java, LayoutFactory({ ItemViewHolder(it, onItemClick) }, R.layout.signal_context_menu_item))
|
||||
}
|
||||
|
||||
init {
|
||||
recyclerView.apply {
|
||||
adapter = mappingAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
itemAnimator = null
|
||||
}
|
||||
}
|
||||
|
||||
fun setItems(items: List<ActionItem>) {
|
||||
mappingAdapter.submitList(items.toAdapterItems())
|
||||
}
|
||||
|
||||
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
|
||||
return this.mapIndexed { index, item ->
|
||||
val displayType: DisplayType = when {
|
||||
this.size == 1 -> DisplayType.ONLY
|
||||
index == 0 -> DisplayType.TOP
|
||||
index == this.size - 1 -> DisplayType.BOTTOM
|
||||
else -> DisplayType.MIDDLE
|
||||
}
|
||||
|
||||
DisplayItem(item, displayType)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DisplayItem(
|
||||
val item: ActionItem,
|
||||
val displayType: DisplayType
|
||||
) : MappingModel<DisplayItem> {
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
}
|
||||
|
||||
private enum class DisplayType {
|
||||
TOP, BOTTOM, MIDDLE, ONLY
|
||||
}
|
||||
|
||||
private class ItemViewHolder(
|
||||
itemView: View,
|
||||
private val onItemClick: () -> Unit,
|
||||
) : MappingViewHolder<DisplayItem>(itemView) {
|
||||
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
|
||||
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
|
||||
|
||||
override fun bind(model: DisplayItem) {
|
||||
icon.setImageResource(model.item.iconRes)
|
||||
title.text = model.item.title
|
||||
itemView.setOnClickListener {
|
||||
model.item.action.run()
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
|
||||
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
|
||||
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import android.view.animation.AnimationUtils
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isGone
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
@@ -64,14 +63,11 @@ class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : Line
|
||||
}
|
||||
|
||||
private fun present(items: List<ActionItem>) {
|
||||
if (isGone) {
|
||||
if (width == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (width == 0) {
|
||||
post { present(items) }
|
||||
return
|
||||
}
|
||||
val wasLayoutRequested = isLayoutRequested
|
||||
|
||||
val widthDp: Float = ViewUtil.pxToDp(width.toFloat())
|
||||
val minButtonWidthDp = 80
|
||||
@@ -109,6 +105,12 @@ class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : Line
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (wasLayoutRequested) {
|
||||
post {
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindItem(view: View, item: ActionItem) {
|
||||
|
||||
@@ -6,16 +6,9 @@ import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
@@ -41,9 +34,10 @@ class SignalContextMenu private constructor(
|
||||
|
||||
val context: Context = anchor.context
|
||||
|
||||
val mappingAdapter = MappingAdapter().apply {
|
||||
registerFactory(DisplayItem::class.java, ItemViewHolderFactory())
|
||||
}
|
||||
private val contextMenuList = ContextMenuList(
|
||||
recyclerView = contentView.findViewById(R.id.signal_context_menu_list),
|
||||
onItemClick = { dismiss() },
|
||||
)
|
||||
|
||||
init {
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||
@@ -58,13 +52,7 @@ class SignalContextMenu private constructor(
|
||||
elevation = 20f
|
||||
}
|
||||
|
||||
contentView.findViewById<RecyclerView>(R.id.signal_context_menu_list).apply {
|
||||
adapter = mappingAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
mappingAdapter.submitList(items.toAdapterItems())
|
||||
contextMenuList.setItems(items)
|
||||
}
|
||||
|
||||
private fun show() {
|
||||
@@ -96,7 +84,7 @@ class SignalContextMenu private constructor(
|
||||
offsetY = baseOffsetY
|
||||
} else if (menuTopBound > screenTopBound) {
|
||||
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||
mappingAdapter.submitList(items.reversed().toAdapterItems())
|
||||
contextMenuList.setItems(items.reversed())
|
||||
} else {
|
||||
offsetY = -((anchorRect.height() / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
|
||||
}
|
||||
@@ -121,65 +109,6 @@ class SignalContextMenu private constructor(
|
||||
showAsDropDown(anchor, offsetX, offsetY)
|
||||
}
|
||||
|
||||
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
|
||||
return this.mapIndexed { index, item ->
|
||||
val displayType: DisplayType = when {
|
||||
this.size == 1 -> DisplayType.ONLY
|
||||
index == 0 -> DisplayType.TOP
|
||||
index == this.size - 1 -> DisplayType.BOTTOM
|
||||
else -> DisplayType.MIDDLE
|
||||
}
|
||||
|
||||
DisplayItem(item, displayType)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DisplayItem(
|
||||
val item: ActionItem,
|
||||
val displayType: DisplayType
|
||||
) : MappingModel<DisplayItem> {
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
}
|
||||
|
||||
private enum class DisplayType {
|
||||
TOP, BOTTOM, MIDDLE, ONLY
|
||||
}
|
||||
|
||||
private inner class ItemViewHolder(itemView: View) : MappingViewHolder<DisplayItem>(itemView) {
|
||||
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
|
||||
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
|
||||
|
||||
override fun bind(model: DisplayItem) {
|
||||
icon.setImageResource(model.item.iconRes)
|
||||
title.text = model.item.title
|
||||
itemView.setOnClickListener {
|
||||
model.item.action.run()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
|
||||
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
|
||||
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ItemViewHolderFactory : MappingAdapter.Factory<DisplayItem> {
|
||||
override fun createViewHolder(parent: ViewGroup): MappingViewHolder<DisplayItem> {
|
||||
return ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.signal_context_menu_item, parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
enum class HorizontalPosition {
|
||||
START, END
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package org.thoughtcrime.securesms.components.settings;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
|
||||
|
||||
/**
|
||||
* Reusable adapter for generic settings list.
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModelList;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -7,8 +7,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
@@ -18,9 +18,10 @@ import org.thoughtcrime.securesms.components.settings.models.Button
|
||||
import org.thoughtcrime.securesms.components.settings.models.Space
|
||||
import org.thoughtcrime.securesms.components.settings.models.Text
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class DSLSettingsAdapter : MappingAdapter() {
|
||||
|
||||
@@ -22,7 +22,7 @@ abstract class DSLSettingsFragment(
|
||||
@StringRes private val titleId: Int = -1,
|
||||
@MenuRes private val menuId: Int = -1,
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment,
|
||||
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
||||
protected var layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
||||
) : Fragment(layoutId) {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.StyleRes
|
||||
@@ -81,4 +82,17 @@ sealed class DSLSettingsText {
|
||||
return SpanUtil.bold(charSequence)
|
||||
}
|
||||
}
|
||||
|
||||
class LearnMoreModifier(
|
||||
@ColorInt private val learnMoreColor: Int,
|
||||
val onClick: () -> Unit
|
||||
) : Modifier {
|
||||
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||
return SpannableStringBuilder(charSequence).append(" ").append(
|
||||
SpanUtil.learnMore(context, learnMoreColor) {
|
||||
onClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
|
||||
|
||||
/**
|
||||
* Simple progress indicator that can be used multiple times (if provided with different {@link Item#id}s).
|
||||
|
||||
@@ -10,8 +10,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.navigation.NavDirections
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
@@ -15,14 +16,18 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.CachedInflater
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private const val START_LOCATION = "app.settings.start.location"
|
||||
private const val START_ARGUMENTS = "app.settings.start.arguments"
|
||||
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
|
||||
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
|
||||
private const val EXTRA_PERFORM_ACTION_ON_CREATE = "extra_perform_action_on_create"
|
||||
|
||||
class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
|
||||
@@ -65,7 +70,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
}
|
||||
|
||||
startingAction?.let {
|
||||
navController.navigate(it)
|
||||
navController.safeNavigate(it)
|
||||
}
|
||||
|
||||
SignalStore.settings().onConfigurationSettingChanged.observe(this) { key ->
|
||||
@@ -81,6 +86,17 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
when (intent.getStringExtra(EXTRA_PERFORM_ACTION_ON_CREATE)) {
|
||||
ACTION_CHANGE_NUMBER_SUCCESS -> {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.ChangeNumber__your_phone_number_has_changed_to_s, PhoneNumberFormatter.prettyPrint(Recipient.self().requireE164())))
|
||||
.setPositiveButton(R.string.ChangeNumber__okay, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
@@ -108,8 +124,14 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_CHANGE_NUMBER_SUCCESS = "action_change_number_success"
|
||||
|
||||
@JvmStatic
|
||||
fun home(context: Context): Intent = getIntentForStartLocation(context, StartLocation.HOME)
|
||||
@JvmOverloads
|
||||
fun home(context: Context, action: String? = null): Intent {
|
||||
return getIntentForStartLocation(context, StartLocation.HOME)
|
||||
.putExtra(EXTRA_PERFORM_ACTION_ON_CREATE, action)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun backups(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS)
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.settings.app
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
@@ -22,9 +21,10 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__menu_settings) {
|
||||
|
||||
@@ -35,9 +35,9 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
adapter.registerFactory(BioPreference::class.java, MappingAdapter.LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
|
||||
adapter.registerFactory(PaymentsPreference::class.java, MappingAdapter.LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
|
||||
adapter.registerFactory(SubscriptionPreference::class.java, MappingAdapter.LayoutFactory(::SubscriptionPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
adapter.registerFactory(BioPreference::class.java, LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
|
||||
adapter.registerFactory(PaymentsPreference::class.java, LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
|
||||
adapter.registerFactory(SubscriptionPreference::class.java, LayoutFactory(::SubscriptionPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
@@ -54,7 +54,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
customPref(
|
||||
BioPreference(state.self) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__account),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_profile_circle_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.preferences__linked_devices),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_linked_devices_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_deviceActivity)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_deviceActivity)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -79,7 +79,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
PaymentsPreference(
|
||||
unreadCount = state.unreadPaymentsCount
|
||||
) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_paymentsActivity)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_paymentsActivity)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.preferences__appearance),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_appearance_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__chats),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_message_tinted_bitmap_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -106,7 +106,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.preferences__notifications),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_bell_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,7 +114,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.preferences__privacy),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -122,7 +122,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.preferences__data_and_storage),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_archive_24dp),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -132,7 +132,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.preferences__help),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_help_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,7 +140,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.AppSettingsFragment__invite_your_friends),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_invite_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_inviteActivity)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_inviteActivity)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -158,9 +158,9 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
isActive = state.hasActiveSubscription,
|
||||
onClick = { isActive ->
|
||||
if (isActive) {
|
||||
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToManageDonationsFragment())
|
||||
findNavController().safeNavigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToManageDonationsFragment())
|
||||
} else {
|
||||
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToSubscribeFragment())
|
||||
findNavController().safeNavigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToSubscribeFragment())
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -169,7 +169,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
title = DSLSettingsText.from(R.string.preferences__signal_boost),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_boost_24),
|
||||
onClick = {
|
||||
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment())
|
||||
findNavController().safeNavigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@@ -186,7 +186,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_preferences),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_internalSettingsFragment)
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_internalSettingsFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFragment__account) {
|
||||
|
||||
@@ -98,7 +99,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__advanced_pin_settings),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -106,11 +107,11 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
|
||||
sectionHeaderPref(R.string.AccountSettingsFragment__account)
|
||||
|
||||
if (FeatureFlags.changeNumber() && Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED) {
|
||||
if (FeatureFlags.changeNumber() && Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED && SignalStore.account().isRegistered) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -119,14 +120,14 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
|
||||
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), R.color.signal_alert_primary)),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.util.navigation.safeNavigate
|
||||
|
||||
class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__appearance) {
|
||||
|
||||
@@ -44,7 +45,7 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appearanceSettings_to_wallpaperActivity)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_appearanceSettings_to_wallpaperActivity)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_number_confirm) {
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
@@ -28,6 +29,6 @@ class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_num
|
||||
editNumber.setOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
val changeNumber: View = view.findViewById(R.id.change_number_confirm_change_number)
|
||||
changeNumber.setOnClickListener { findNavController().navigate(R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment) }
|
||||
changeNumber.setOnClickListener { findNavController().safeNavigate(R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment
|
||||
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragmentArgs
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationNumberInputController
|
||||
import org.thoughtcrime.securesms.util.Dialogs
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country"
|
||||
private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country"
|
||||
@@ -73,7 +74,7 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
|
||||
override fun onPickCountry(view: View) {
|
||||
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(OLD_NUMBER_COUNTRY_SELECT).build()
|
||||
|
||||
findNavController().navigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
@@ -110,7 +111,7 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
|
||||
override fun onPickCountry(view: View) {
|
||||
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(NEW_NUMBER_COUNTRY_SELECT).build()
|
||||
|
||||
findNavController().navigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
@@ -157,7 +158,7 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
|
||||
}
|
||||
|
||||
when (viewModel.canContinue()) {
|
||||
ContinueStatus.CAN_CONTINUE -> findNavController().navigate(R.id.action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment)
|
||||
ContinueStatus.CAN_CONTINUE -> findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment)
|
||||
ContinueStatus.INVALID_NUMBER -> {
|
||||
Dialogs.showAlertDialog(
|
||||
context, getString(R.string.RegistrationActivity_invalid_number), String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.number.e164Number)
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseEnterSmsCodeFragment
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberViewModel>(R.layout.fragment_change_number_enter_code) {
|
||||
|
||||
@@ -50,14 +51,14 @@ class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberVi
|
||||
}
|
||||
|
||||
override fun navigateToCaptcha() {
|
||||
findNavController().navigate(R.id.action_changeNumberEnterCodeFragment_to_captchaFragment, getCaptchaArguments())
|
||||
findNavController().safeNavigate(R.id.action_changeNumberEnterCodeFragment_to_captchaFragment, getCaptchaArguments())
|
||||
}
|
||||
|
||||
override fun navigateToRegistrationLock(timeRemaining: Long) {
|
||||
findNavController().navigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
findNavController().safeNavigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
}
|
||||
|
||||
override fun navigateToKbsAccountLocked() {
|
||||
findNavController().navigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
findNavController().safeNavigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class ChangeNumberFragment : LoggingFragment(R.layout.fragment_change_phone_number) {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -13,7 +14,7 @@ class ChangeNumberFragment : LoggingFragment(R.layout.fragment_change_phone_numb
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.change_phone_number_continue).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment)
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import java.util.Objects
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
||||
@@ -57,7 +58,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number)
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni))
|
||||
.map { true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewMod
|
||||
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layout.fragment_change_number_registration_lock) {
|
||||
|
||||
@@ -38,7 +39,7 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo
|
||||
}
|
||||
|
||||
override fun navigateToAccountLocked() {
|
||||
findNavController().navigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked())
|
||||
findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked())
|
||||
}
|
||||
|
||||
override fun handleSuccessfulPinEntry(pin: String) {
|
||||
@@ -47,7 +48,7 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo
|
||||
cancelSpinning(pinButton)
|
||||
|
||||
if (pinsDiffer) {
|
||||
findNavController().navigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
|
||||
findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
|
||||
} else {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
|
||||
@@ -12,12 +12,17 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
|
||||
|
||||
@@ -47,6 +52,8 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: KeyBackupSystemNoDataException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: IOException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
@@ -58,10 +65,26 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun changeLocalNumber(e164: String): Single<Unit> {
|
||||
fun changeLocalNumber(e164: String, pni: PNI): Single<Unit> {
|
||||
val oldStorageId: ByteArray? = Recipient.self().storageServiceId
|
||||
SignalDatabase.recipients.updateSelfPhone(e164)
|
||||
val newStorageId: ByteArray? = Recipient.self().storageServiceId
|
||||
|
||||
if (MessageDigest.isEqual(oldStorageId, newStorageId)) {
|
||||
Log.w(TAG, "Self storage id was not rotated, attempting to rotate again")
|
||||
SignalDatabase.recipients.rotateStorageId(Recipient.self().id)
|
||||
Recipient.self().live().refresh()
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
val secondAttemptStorageId: ByteArray? = Recipient.self().storageServiceId
|
||||
if (MessageDigest.isEqual(oldStorageId, secondAttemptStorageId)) {
|
||||
Log.w(TAG, "Second attempt also failed to rotate storage id")
|
||||
}
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.setPni(Recipient.self().id, pni)
|
||||
|
||||
SignalStore.account().setE164(e164)
|
||||
SignalStore.account().setPni(pni)
|
||||
|
||||
ApplicationDependencies.closeConnections()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.registration.fragments.CaptchaFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
|
||||
@@ -35,7 +34,7 @@ object ChangeNumberUtil {
|
||||
}
|
||||
|
||||
fun Fragment.changeNumberSuccess() {
|
||||
findNavController().navigate(R.id.action_pop_app_settings_change_number)
|
||||
Toast.makeText(requireContext(), R.string.ChangeNumber__your_phone_number_has_been_changed, Toast.LENGTH_SHORT).show()
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.home(requireContext(), AppSettingsActivity.ACTION_CHANGE_NUMBER_SUCCESS))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java)
|
||||
|
||||
@@ -52,13 +53,13 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit")
|
||||
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired()) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required")
|
||||
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
requestingCaptcha = true
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.registration.VerifyProcessor
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.util.Objects
|
||||
@@ -145,7 +146,7 @@ class ChangeNumberViewModel(
|
||||
|
||||
@WorkerThread
|
||||
override fun onVerifySuccess(processor: VerifyAccountResponseProcessor): Single<VerifyAccountResponseProcessor> {
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number)
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseOrThrow(processor.result.pni))
|
||||
.map { processor }
|
||||
.onErrorReturn { t ->
|
||||
Log.w(TAG, "Error attempting to change local number", t)
|
||||
@@ -154,7 +155,7 @@ class ChangeNumberViewModel(
|
||||
}
|
||||
|
||||
override fun onVerifySuccessWithRegistrationLock(processor: VerifyCodeWithRegistrationLockResponseProcessor, pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number)
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseOrThrow(processor.result.verifyAccountResponse.pni))
|
||||
.map { processor }
|
||||
.onErrorReturn { t ->
|
||||
Log.w(TAG, "Error attempting to change local number", t)
|
||||
|
||||
@@ -8,11 +8,17 @@ 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.util.navigation.safeNavigate
|
||||
|
||||
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
|
||||
|
||||
private lateinit var viewModel: ChatsSettingsViewModel
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
val repository = ChatsSettingsRepository()
|
||||
val factory = ChatsSettingsViewModel.Factory(repository)
|
||||
@@ -29,7 +35,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__sms_mms),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_chatsSettingsFragment_to_smsSettingsFragment)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_smsSettingsFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -79,7 +85,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__chat_backups),
|
||||
summary = DSLSettingsText.from(if (state.chatBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
@@ -29,9 +28,6 @@ class ChatsSettingsRepository {
|
||||
isLinkPreviewsEnabled
|
||||
)
|
||||
)
|
||||
if (isLinkPreviewsEnabled) {
|
||||
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.LINK_PREVIEWS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,13 @@ class ChatsSettingsViewModel(private val repository: ChatsSettingsRepository) :
|
||||
SignalStore.settings().isEnterKeySends = enabled
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
val backupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
|
||||
if (store.state.chatBackupsEnabled != backupsEnabled) {
|
||||
store.update { it.copy(chatBackupsEnabled = backupsEnabled) }
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(private val repository: ChatsSettingsRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(ChatsSettingsViewModel(repository)))
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.SmsUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private const val SMS_REQUEST_CODE: Short = 1234
|
||||
|
||||
@@ -76,7 +77,7 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__advanced_mms_access_point_names),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_smsSettingsFragment_to_mmsPreferencesFragment)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_smsSettingsFragment_to_mmsPreferencesFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -47,7 +48,7 @@ class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences_
|
||||
title = DSLSettingsText.from(R.string.preferences_data_and_storage__manage_storage),
|
||||
summary = DSLSettingsText.from(Util.getPrettyFileSize(state.totalStorageUse)),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_dataAndStorageSettingsFragment_to_storagePreferenceFragment)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_storagePreferenceFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -125,7 +126,7 @@ class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences_
|
||||
title = DSLSettingsText.from(R.string.preferences_use_proxy),
|
||||
summary = DSLSettingsText.from(if (state.isProxyEnabled) R.string.preferences_on else R.string.preferences_off),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_dataAndStorageSettingsFragment_to_editProxyFragment)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_editProxyFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.util.navigation.safeNavigate
|
||||
|
||||
class HelpSettingsFragment : DSLSettingsFragment(R.string.preferences__help) {
|
||||
|
||||
@@ -25,7 +26,7 @@ class HelpSettingsFragment : DSLSettingsFragment(R.string.preferences__help) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.HelpSettingsFragment__contact_us),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_helpSettingsFragment_to_helpFragment)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_helpSettingsFragment_to_helpFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -39,7 +40,7 @@ class HelpSettingsFragment : DSLSettingsFragment(R.string.preferences__help) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.HelpSettingsFragment__debug_log),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_helpSettingsFragment_to_submitDebugLogActivity)
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_helpSettingsFragment_to_submitDebugLogActivity)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
|
||||
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
@@ -31,6 +32,7 @@ import org.thoughtcrime.securesms.payments.DataExportUtil
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
|
||||
import kotlin.math.max
|
||||
|
||||
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
|
||||
|
||||
@@ -333,9 +335,9 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
if (FeatureFlags.donorBadges() && SignalStore.donationsValues().getSubscriber() != null) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_badges)
|
||||
|
||||
clickPref(
|
||||
@@ -345,6 +347,25 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_release_channel)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_fetch_release_channel),
|
||||
onClick = {
|
||||
SignalStore.releaseChannelValues().previousManifestMd5 = ByteArray(0)
|
||||
RetrieveReleaseChannelJob.enqueue(force = true)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_release_channel_set_last_version),
|
||||
onClick = {
|
||||
SignalStore.releaseChannelValues().highestVersionNoteReceived = max(SignalStore.releaseChannelValues().highestVersionNoteReceived - 10, 0)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.media.Ringtone
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
@@ -17,6 +18,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
@@ -29,12 +31,14 @@ import org.thoughtcrime.securesms.components.settings.RadioListPreferenceViewHol
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.RingtoneUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private const val MESSAGE_SOUND_SELECT: Int = 1
|
||||
private const val CALL_RINGTONE_SELECT: Int = 2
|
||||
private val TAG = Log.tag(NotificationsSettingsFragment::class.java)
|
||||
|
||||
class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__notifications) {
|
||||
|
||||
@@ -68,7 +72,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
adapter.registerFactory(
|
||||
LedColorPreference::class.java,
|
||||
MappingAdapter.LayoutFactory(::LedColorPreferenceViewHolder, R.layout.dsl_preference_item)
|
||||
LayoutFactory(::LedColorPreferenceViewHolder, R.layout.dsl_preference_item)
|
||||
)
|
||||
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
@@ -223,9 +227,9 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.NotificationsSettingsFragment__profiles),
|
||||
summary = DSLSettingsText.from(R.string.NotificationsSettingsFragment__set_up_notification_profiles),
|
||||
summary = DSLSettingsText.from(R.string.NotificationsSettingsFragment__create_a_profile_to_receive_notifications_only_from_people_and_groups_you_choose),
|
||||
onClick = {
|
||||
findNavController().navigate(R.id.action_notificationsSettingsFragment_to_notificationProfilesFragment)
|
||||
findNavController().safeNavigate(R.id.action_notificationsSettingsFragment_to_notificationProfilesFragment)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -247,9 +251,14 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
return if (TextUtils.isEmpty(uri.toString())) {
|
||||
getString(R.string.preferences__silent)
|
||||
} else {
|
||||
val tone = RingtoneUtil.getRingtone(requireContext(), uri)
|
||||
val tone: Ringtone? = RingtoneUtil.getRingtone(requireContext(), uri)
|
||||
if (tone != null) {
|
||||
tone.getTitle(requireContext()) ?: getString(R.string.NotificationsSettingsFragment__unknown_ringtone)
|
||||
try {
|
||||
tone.getTitle(requireContext()) ?: getString(R.string.NotificationsSettingsFragment__unknown_ringtone)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Unable to get title for ringtone", e)
|
||||
return getString(R.string.NotificationsSettingsFragment__unknown_ringtone)
|
||||
}
|
||||
} else {
|
||||
getString(R.string.preferences__default)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class NotificationProfileSelectionFragment : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
return configure {
|
||||
|
||||
state.notificationProfiles.forEach { profile ->
|
||||
state.notificationProfiles.sortedDescending().forEach { profile ->
|
||||
customPref(
|
||||
NotificationProfileSelection.Entry(
|
||||
isOn = profile == activeProfile,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.notifications.manual
|
||||
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import java.util.Calendar
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class NotificationProfileSelectionState(
|
||||
val notificationProfiles: List<NotificationProfile> = listOf(),
|
||||
val expandedId: Long = -1L,
|
||||
val timeSlotB: Calendar
|
||||
val timeSlotB: LocalDateTime
|
||||
)
|
||||
|
||||
@@ -9,8 +9,10 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.util.isBetween
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.util.Calendar
|
||||
import org.thoughtcrime.securesms.util.toMillis
|
||||
import java.time.LocalDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NotificationProfileSelectionViewModel(private val repository: NotificationProfilesRepository) : ViewModel() {
|
||||
@@ -53,29 +55,24 @@ class NotificationProfileSelectionViewModel(private val repository: Notification
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
fun enableUntil(profile: NotificationProfile, calendar: Calendar) {
|
||||
disposables += repository.manuallyEnableProfileForDuration(profile.id, calendar.timeInMillis)
|
||||
fun enableUntil(profile: NotificationProfile, enableUntil: LocalDateTime) {
|
||||
disposables += repository.manuallyEnableProfileForDuration(profile.id, enableUntil.toMillis())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun getTimeSlotB(): Calendar {
|
||||
val now = Calendar.getInstance()
|
||||
val sixPm = Calendar.getInstance()
|
||||
val eightAm = Calendar.getInstance()
|
||||
@Suppress("CascadeIf")
|
||||
private fun getTimeSlotB(): LocalDateTime {
|
||||
val now = LocalDateTime.now()
|
||||
val sixPm = now.withHour(18).withMinute(0).withSecond(0)
|
||||
val eightAm = now.withHour(8).withMinute(0).withSecond(0)
|
||||
|
||||
sixPm.set(Calendar.HOUR_OF_DAY, 18)
|
||||
sixPm.set(Calendar.MINUTE, 0)
|
||||
sixPm.set(Calendar.SECOND, 0)
|
||||
|
||||
eightAm.set(Calendar.HOUR_OF_DAY, 8)
|
||||
eightAm.set(Calendar.MINUTE, 0)
|
||||
eightAm.set(Calendar.SECOND, 0)
|
||||
|
||||
return if (now.before(sixPm) && (now.after(eightAm) || now == eightAm)) {
|
||||
return if (now.isBetween(eightAm, sixPm)) {
|
||||
sixPm
|
||||
} else {
|
||||
} else if (now.isBefore(eightAm)) {
|
||||
eightAm
|
||||
} else {
|
||||
eightAm.plusDays(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.formatHours
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
|
||||
/**
|
||||
* Notification Profile selection preference.
|
||||
@@ -25,8 +26,8 @@ object NotificationProfileSelection {
|
||||
private const val UPDATE_TIMESLOT = 1
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(New::class.java, MappingAdapter.LayoutFactory(::NewViewHolder, R.layout.new_notification_profile_pref))
|
||||
adapter.registerFactory(Entry::class.java, MappingAdapter.LayoutFactory(::EntryViewHolder, R.layout.notification_profile_entry_pref))
|
||||
adapter.registerFactory(New::class.java, LayoutFactory(::NewViewHolder, R.layout.new_notification_profile_pref))
|
||||
adapter.registerFactory(Entry::class.java, LayoutFactory(::EntryViewHolder, R.layout.notification_profile_entry_pref))
|
||||
}
|
||||
|
||||
class Entry(
|
||||
@@ -34,10 +35,10 @@ object NotificationProfileSelection {
|
||||
override val summary: DSLSettingsText,
|
||||
val notificationProfile: NotificationProfile,
|
||||
val isExpanded: Boolean,
|
||||
val timeSlotB: Calendar,
|
||||
val timeSlotB: LocalDateTime,
|
||||
val onRowClick: (NotificationProfile) -> Unit,
|
||||
val onTimeSlotAClick: (NotificationProfile) -> Unit,
|
||||
val onTimeSlotBClick: (NotificationProfile, Calendar) -> Unit,
|
||||
val onTimeSlotBClick: (NotificationProfile, LocalDateTime) -> Unit,
|
||||
val onViewSettingsClick: (NotificationProfile) -> Unit,
|
||||
val onToggleClick: (NotificationProfile) -> Unit
|
||||
) : PreferenceModel<Entry>() {
|
||||
@@ -87,7 +88,7 @@ object NotificationProfileSelection {
|
||||
expansion.visible = model.isExpanded
|
||||
timeSlotB.text = context.getString(
|
||||
R.string.NotificationProfileSelection__until_s,
|
||||
DateUtils.getTimeString(context, Locale.getDefault(), model.timeSlotB.timeInMillis)
|
||||
LocalTime.from(model.timeSlotB).formatHours(context)
|
||||
)
|
||||
|
||||
if (TOGGLE_EXPANSION in payload || UPDATE_TIMESLOT in payload) {
|
||||
@@ -107,7 +108,7 @@ object NotificationProfileSelection {
|
||||
|
||||
timeSlotB.text = context.getString(
|
||||
R.string.NotificationProfileSelection__until_s,
|
||||
DateUtils.getTimeString(context, Locale.getDefault(), model.timeSlotB.timeInMillis)
|
||||
LocalTime.from(model.timeSlotB).formatHours(context)
|
||||
)
|
||||
|
||||
itemView.isSelected = model.isOn
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Show and allow addition of recipients to a profile during the create flow.
|
||||
@@ -38,7 +39,7 @@ class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragme
|
||||
|
||||
view.findViewById<CircularProgressButton>(R.id.add_allowed_members_profile_next).apply {
|
||||
setOnClickListener {
|
||||
findNavController().navigate(AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToEditNotificationProfileScheduleFragment(profileId, true))
|
||||
findNavController().safeNavigate(AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToEditNotificationProfileScheduleFragment(profileId, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +63,7 @@ class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragme
|
||||
customPref(
|
||||
NotificationProfileAddMembers.Model(
|
||||
onClick = { id, currentSelection ->
|
||||
findNavController().navigate(
|
||||
findNavController().safeNavigate(
|
||||
AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToSelectRecipientsFragment(id)
|
||||
.setCurrentSelection(currentSelection.toTypedArray())
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged
|
||||
|
||||
/**
|
||||
@@ -104,7 +105,7 @@ class EditNotificationProfileFragment : DSLSettingsFragment(layoutId = R.layout.
|
||||
is SaveNotificationProfileResult.Success -> {
|
||||
ViewUtil.hideKeyboard(requireContext(), nameView)
|
||||
if (saveResult.createMode) {
|
||||
findNavController().navigate(EditNotificationProfileFragmentDirections.actionEditNotificationProfileFragmentToAddAllowedMembersFragment(saveResult.profile.id))
|
||||
findNavController().safeNavigate(EditNotificationProfileFragmentDirections.actionEditNotificationProfileFragmentToAddAllowedMembersFragment(saveResult.profile.id))
|
||||
} else {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
@@ -121,7 +122,7 @@ class EditNotificationProfileFragment : DSLSettingsFragment(layoutId = R.layout.
|
||||
.subscribeBy(
|
||||
onSuccess = { initial ->
|
||||
if (initial.createMode) {
|
||||
saveButton.text = getString(R.string.EditNotificationProfileFragment__next)
|
||||
saveButton.text = getString(R.string.EditNotificationProfileFragment__create)
|
||||
title.setText(R.string.EditNotificationProfileFragment__name_your_profile)
|
||||
} else {
|
||||
saveButton.text = getString(R.string.EditNotificationProfileFragment__save)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user