Compare commits
222 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45267f3590 | ||
|
|
3fb8c6eda8 | ||
|
|
27ce0fd65e | ||
|
|
7e91132e7e | ||
|
|
705839068a | ||
|
|
6625ac02d5 | ||
|
|
4b3580d98a | ||
|
|
6dbbec2631 | ||
|
|
a7b6ebe7fc | ||
|
|
76f52b9086 | ||
|
|
3310246351 | ||
|
|
f3d0b4a671 | ||
|
|
83b9fbac11 | ||
|
|
5ca843825f | ||
|
|
e92c83401b | ||
|
|
e18d9e665f | ||
|
|
cc99febe32 | ||
|
|
e72be42eff | ||
|
|
bad382e2f3 | ||
|
|
e637f15a43 | ||
|
|
6c55916cda | ||
|
|
fbabab0b70 | ||
|
|
e268887255 | ||
|
|
6b07922757 | ||
|
|
a464e57079 | ||
|
|
b5af691cc4 | ||
|
|
5c1b57e4ba | ||
|
|
a5c51ff801 | ||
|
|
d755e1e29e | ||
|
|
b9361112b6 | ||
|
|
32101f7dda | ||
|
|
29e697265c | ||
|
|
4cd9ccc0f1 | ||
|
|
8936d81bc7 | ||
|
|
cc36f83d77 | ||
|
|
64996a8db7 | ||
|
|
7267d77dcb | ||
|
|
2281e83607 | ||
|
|
e6b03b1a4a | ||
|
|
fb86fdfcd9 | ||
|
|
77cf029fdc | ||
|
|
556ca5a573 | ||
|
|
91645e6adc | ||
|
|
4d6bb95aa4 | ||
|
|
55ee68fa2d | ||
|
|
747bc7c3bf | ||
|
|
9c17201eaf | ||
|
|
a24b3d9a60 | ||
|
|
c93d882fe1 | ||
|
|
67403a6a9f | ||
|
|
5f9e72bb3c | ||
|
|
091b38ceb8 | ||
|
|
83dfb984fb | ||
|
|
9f14831fc4 | ||
|
|
48bfcc9b16 | ||
|
|
7028ca9411 | ||
|
|
5175375483 | ||
|
|
e2dbaa605b | ||
|
|
93fd6e7a55 | ||
|
|
b070e6962f | ||
|
|
b1d1b7e31e | ||
|
|
1a5ae603d5 | ||
|
|
3b42bda63d | ||
|
|
2f70a71a6c | ||
|
|
aae368c049 | ||
|
|
dee3d2ff2d | ||
|
|
da2e2a99af | ||
|
|
0c00426c0c | ||
|
|
ccc96d5bfa | ||
|
|
82c12c2f6b | ||
|
|
9416beb4aa | ||
|
|
39b80a48c7 | ||
|
|
d5491a2e84 | ||
|
|
07b19402e6 | ||
|
|
318b4703f2 | ||
|
|
d389697f27 | ||
|
|
7bcc338a49 | ||
|
|
ce2c2002c6 | ||
|
|
d5fbd10406 | ||
|
|
6f6da699a3 | ||
|
|
62d8c115ba | ||
|
|
fd01ee2a87 | ||
|
|
9aa517ad99 | ||
|
|
6c3e1b6a29 | ||
|
|
5d5063ef5f | ||
|
|
b48668455c | ||
|
|
18ba5fa291 | ||
|
|
5e968eb831 | ||
|
|
b4465953d8 | ||
|
|
08a7da3339 | ||
|
|
c1a08616ab | ||
|
|
3761859681 | ||
|
|
8984b763fb | ||
|
|
59c62671b9 | ||
|
|
bcfe8909e5 | ||
|
|
c43fe44e3e | ||
|
|
4ac1134a9b | ||
|
|
08d03cb456 | ||
|
|
e5c172a819 | ||
|
|
4569011e0b | ||
|
|
1031a4e96c | ||
|
|
cdf8e4e1ed | ||
|
|
b589449c34 | ||
|
|
7d7dd101df | ||
|
|
e687fea567 | ||
|
|
e2cb522e87 | ||
|
|
662ba85c5a | ||
|
|
d29ebc7768 | ||
|
|
95fabd7ed1 | ||
|
|
5d5251054c | ||
|
|
c766ba9808 | ||
|
|
8b5fe79849 | ||
|
|
903c5c6db6 | ||
|
|
e2e0caa94a | ||
|
|
520fe481e9 | ||
|
|
7262aefa34 | ||
|
|
8815cdc3de | ||
|
|
8df86962e9 | ||
|
|
573c0fad7f | ||
|
|
a8419d5f02 | ||
|
|
6088f16e3a | ||
|
|
1a5ae592a7 | ||
|
|
6880dfeb62 | ||
|
|
dfecb0efd8 | ||
|
|
2eaadd4337 | ||
|
|
655f3c1219 | ||
|
|
1494a3559d | ||
|
|
f61d7a9f77 | ||
|
|
ee0ab8f035 | ||
|
|
6e85c74e3f | ||
|
|
3b1aa5b176 | ||
|
|
715ad0d459 | ||
|
|
05f7dce503 | ||
|
|
ecfbeb69c5 | ||
|
|
c7fb343b93 | ||
|
|
b0d0814b88 | ||
|
|
9f3765d368 | ||
|
|
fe82c4e487 | ||
|
|
e9bbb1b9ae | ||
|
|
fd3e88707c | ||
|
|
8e8def8b03 | ||
|
|
5b1069018f | ||
|
|
abc71f4fb4 | ||
|
|
2f5e95c0e3 | ||
|
|
d1fd70a807 | ||
|
|
1b924c606a | ||
|
|
9b175fa0dd | ||
|
|
7e7bbad788 | ||
|
|
d8c82add78 | ||
|
|
6e1657b1bd | ||
|
|
1f7b1d91c4 | ||
|
|
e7833df539 | ||
|
|
fae80a242d | ||
|
|
77ff25ec49 | ||
|
|
bb446ac1d5 | ||
|
|
b6a4d01d42 | ||
|
|
bd4dd25460 | ||
|
|
f86c1fe508 | ||
|
|
38f6efbcae | ||
|
|
30a542234b | ||
|
|
8c9bf678fa | ||
|
|
4b465b74e8 | ||
|
|
58a22f0eea | ||
|
|
ddad9acef1 | ||
|
|
1dbb6013cb | ||
|
|
9cc1ae4a29 | ||
|
|
4eb24c3303 | ||
|
|
ec1935572e | ||
|
|
e419d70d51 | ||
|
|
2af5526879 | ||
|
|
6a5aa089ae | ||
|
|
6b5f4ca8c2 | ||
|
|
53e110560a | ||
|
|
82e9c620e8 | ||
|
|
076facbdc2 | ||
|
|
a805f9b6b4 | ||
|
|
969e763997 | ||
|
|
9347227ff5 | ||
|
|
c9ba0432a0 | ||
|
|
e3b7fe7509 | ||
|
|
5332669321 | ||
|
|
a086305c38 | ||
|
|
a712622891 | ||
|
|
1514f91687 | ||
|
|
0dfa6aab09 | ||
|
|
4b6dbac758 | ||
|
|
b816f901a5 | ||
|
|
76d1490810 | ||
|
|
f2ab0b6423 | ||
|
|
e09d162c1e | ||
|
|
c84de8fa60 | ||
|
|
8e020c05f6 | ||
|
|
8c9eb880cf | ||
|
|
d7ddd85a90 | ||
|
|
7d994b2ae1 | ||
|
|
664d6475d9 | ||
|
|
a940487611 | ||
|
|
9f995d61f4 | ||
|
|
a6690e1bde | ||
|
|
d507df2e7e | ||
|
|
fa26eb2017 | ||
|
|
0b53ba8950 | ||
|
|
7447e2497b | ||
|
|
7ac83625d3 | ||
|
|
50dfe7bc25 | ||
|
|
8e32592218 | ||
|
|
a3d72fc06c | ||
|
|
f5a6d61362 | ||
|
|
bca2205945 | ||
|
|
1241f4c0e9 | ||
|
|
f6253ad0bb | ||
|
|
083301185c | ||
|
|
0273d0f285 | ||
|
|
3dc1ce3353 | ||
|
|
f8e077b824 | ||
|
|
aec2ca1d87 | ||
|
|
6e7a18ea11 | ||
|
|
fe54ec9d6c | ||
|
|
1819af3000 | ||
|
|
3c177c4883 | ||
|
|
2c871a36d0 | ||
|
|
6bde55f73b |
4
.github/workflows/android.yml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: set up JDK 1.8
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-version: 11
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
32
.idea/codeStyles/Project.xml
generated
@@ -8,6 +8,38 @@
|
||||
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
|
||||
<option name="ALIGN_MULTILINE_ANNOTATION_PARAMETERS" value="true" />
|
||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
|
||||
@@ -63,4 +63,4 @@ Copyright 2013-2021 Signal
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Google Play and the Google Play logo are trademarks of Google Inc.
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
|
||||
220
app/build.gradle
@@ -11,12 +11,6 @@ apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'app.cash.exhaustive'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
||||
content {
|
||||
includeGroupByRegex "com\\.github\\.chrisbanes.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
|
||||
content {
|
||||
@@ -35,6 +29,10 @@ repositories {
|
||||
includeGroupByRegex "com\\.amulyakhare.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
@@ -43,9 +41,6 @@ repositories {
|
||||
}
|
||||
jcenter {
|
||||
content {
|
||||
includeVersion "com.google.android.exoplayer", "exoplayer-core", "2.9.1"
|
||||
includeVersion "com.google.android.exoplayer", "exoplayer-ui", "2.9.1"
|
||||
includeVersion "com.google.android.exoplayer", "extension-mediasession", "2.9.1"
|
||||
includeVersion "mobi.upod", "time-duration-picker", "1.1.3"
|
||||
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
|
||||
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
|
||||
@@ -71,8 +66,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 908
|
||||
def canonicalVersionName = "5.22.4"
|
||||
def canonicalVersionCode = 930
|
||||
def canonicalVersionName = "5.24.10"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -149,11 +144,12 @@ android {
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSH_URL", "\"https://cdsh.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
@@ -162,6 +158,8 @@ android {
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
@@ -172,7 +170,7 @@ android {
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44}"
|
||||
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44,49,33,41}"
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||
|
||||
@@ -334,7 +332,7 @@ android {
|
||||
|
||||
applicationIdSuffix ".staging"
|
||||
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
@@ -342,7 +340,7 @@ android {
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982\", " +
|
||||
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
@@ -399,153 +397,159 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.5.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.5'
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.fragment.ktx
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring libs.android.tools.desugar
|
||||
|
||||
implementation ('androidx.appcompat:appcompat:1.2.0') {
|
||||
force = true
|
||||
implementation (libs.androidx.appcompat) {
|
||||
version {
|
||||
strictly '1.2.0'
|
||||
}
|
||||
}
|
||||
implementation "androidx.window:window:1.0.0-alpha09"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-reactivestreams-ktx:2.1.0'
|
||||
implementation "androidx.camera:camera-core:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha18"
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
implementation "androidx.autofill:autofill:1.0.0"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||
implementation libs.androidx.window
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.material.material
|
||||
implementation libs.androidx.legacy.support
|
||||
implementation libs.androidx.cardview
|
||||
implementation libs.androidx.preference
|
||||
implementation libs.androidx.legacy.preference
|
||||
implementation libs.androidx.gridlayout
|
||||
implementation libs.androidx.exifinterface
|
||||
implementation libs.androidx.constraintlayout
|
||||
implementation libs.androidx.multidex
|
||||
implementation libs.androidx.navigation.fragment.ktx
|
||||
implementation libs.androidx.navigation.ui.ktx
|
||||
implementation libs.androidx.lifecycle.extensions
|
||||
implementation libs.androidx.lifecycle.viewmodel.savedstate
|
||||
implementation libs.androidx.lifecycle.common.java8
|
||||
implementation libs.androidx.lifecycle.reactivestreams.ktx
|
||||
implementation libs.androidx.camera.core
|
||||
implementation libs.androidx.camera.camera2
|
||||
implementation libs.androidx.camera.lifecycle
|
||||
implementation libs.androidx.camera.view
|
||||
implementation libs.androidx.concurrent.futures
|
||||
implementation libs.androidx.autofill
|
||||
implementation libs.androidx.biometric
|
||||
implementation libs.androidx.sharetarget
|
||||
|
||||
implementation ('com.google.firebase:firebase-messaging:22.0.0') {
|
||||
implementation (libs.firebase.messaging) {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
|
||||
implementation 'com.google.android.gms:play-services-maps:16.1.0'
|
||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||
implementation libs.google.play.services.maps
|
||||
implementation libs.google.play.services.auth
|
||||
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
|
||||
implementation libs.bundles.exoplayer
|
||||
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
implementation libs.conscrypt.android
|
||||
implementation libs.signal.aesgcmprovider
|
||||
|
||||
implementation project(':libsignal-service')
|
||||
implementation project(':paging')
|
||||
implementation project(':core-util')
|
||||
implementation project(':video')
|
||||
implementation project(':device-transfer')
|
||||
implementation project(':image-editor')
|
||||
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
implementation "org.whispersystems:signal-client-android:${LIBSIGNAL_CLIENT_VERSION}"
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
implementation libs.signal.zkgroup.android
|
||||
implementation libs.signal.client.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
|
||||
implementation('com.mobilecoin:android-sdk:1.1.0') {
|
||||
implementation(libs.mobilecoin) {
|
||||
exclude group: 'com.google.protobuf'
|
||||
}
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
implementation(libs.signal.argon2) {
|
||||
artifact {
|
||||
type = "aar"
|
||||
}
|
||||
}
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.11.1'
|
||||
implementation libs.signal.ringrtc
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.22"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
kapt 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
implementation libs.leolin.shortcutbadger
|
||||
implementation libs.emilsjolander.stickylistheaders
|
||||
implementation libs.jpardogo.materialtabstrip
|
||||
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
|
||||
implementation libs.waitingdots
|
||||
implementation libs.floatingactionbutton
|
||||
implementation libs.google.zxing.android.integration
|
||||
implementation libs.time.duration.picker
|
||||
implementation libs.textdrawable
|
||||
implementation libs.google.zxing.core
|
||||
implementation (libs.subsampling.scale.image.view) {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
||||
implementation (libs.numberpickerview) {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
||||
implementation (libs.android.tooltips) {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
|
||||
implementation (libs.android.smsmms) {
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||
}
|
||||
implementation 'com.annimon:stream:1.1.8'
|
||||
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
|
||||
implementation libs.stream
|
||||
implementation (libs.colorpicker) {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
||||
}
|
||||
|
||||
implementation 'com.airbnb.android:lottie:3.6.0'
|
||||
implementation libs.lottie
|
||||
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
implementation libs.stickyheadergrid
|
||||
implementation libs.circular.progress.button
|
||||
|
||||
implementation "net.zetetic:android-database-sqlcipher:4.4.3"
|
||||
implementation "androidx.sqlite:sqlite:2.1.0"
|
||||
implementation libs.signal.android.database.sqlcipher
|
||||
implementation libs.androidx.sqlite
|
||||
|
||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||
implementation (libs.google.ez.vcard) {
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
exclude group: 'org.freemarker'
|
||||
}
|
||||
implementation 'dnsjava:dnsjava:2.1.9'
|
||||
implementation libs.dnsjava
|
||||
|
||||
flipperImplementation 'com.facebook.flipper:flipper:0.91.0'
|
||||
flipperImplementation 'com.facebook.soloader:soloader:0.10.1'
|
||||
flipperImplementation libs.facebook.flipper
|
||||
flipperImplementation libs.facebook.soloader
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:2.8.9'
|
||||
testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
testImplementation testLibs.mockito.core
|
||||
testImplementation testLibs.powermock.api.mockito
|
||||
testImplementation testLibs.powermock.module.junit4.core
|
||||
testImplementation testLibs.powermock.module.junit4.rule
|
||||
testImplementation testLibs.powermock.classloading.xstream
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.4') {
|
||||
testImplementation testLibs.androidx.test.core
|
||||
testImplementation (testLibs.robolectric.robolectric) {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
testImplementation testLibs.robolectric.shadows.multidex
|
||||
testImplementation testLibs.hamcrest.hamcrest
|
||||
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation testLibs.androidx.test.ext.junit
|
||||
androidTestImplementation testLibs.espresso.core
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0"
|
||||
implementation libs.kotlin.stdlib.jdk8
|
||||
implementation libs.kotlin.reflect
|
||||
implementation libs.jackson.module.kotlin
|
||||
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
|
||||
@@ -11,9 +11,9 @@ import androidx.annotation.Nullable;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
||||
|
||||
import net.sqlcipher.DatabaseUtils;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
import net.zetetic.database.DatabaseUtils;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
@@ -176,7 +176,6 @@
|
||||
<activity android:name=".sharing.ShareActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
@@ -318,7 +317,8 @@
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
@@ -366,17 +366,18 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.MediaSendActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
<activity android:name=".mediasend.v2.MediaSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".VerifyIdentityActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".components.settings.app.AppSettingsActivity"
|
||||
@@ -389,6 +390,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.ConversationSettings"
|
||||
|
||||
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 91 KiB |
@@ -60,6 +60,7 @@ import androidx.camera.core.impl.LensFacingConverter;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
@@ -130,6 +131,11 @@ public final class SignalCameraView extends FrameLayout {
|
||||
// For accessibility event
|
||||
private MotionEvent mUpEvent;
|
||||
|
||||
// BEGIN Custom Signal Code Block
|
||||
private Consumer<Throwable> errorConsumer;
|
||||
private Throwable pendingError;
|
||||
// END Custom Signal Code Block
|
||||
|
||||
public SignalCameraView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -167,14 +173,32 @@ public final class SignalCameraView extends FrameLayout {
|
||||
* androidx.lifecycle.Lifecycle.State#DESTROYED} state.
|
||||
* @throws IllegalStateException if camera permissions are not granted.
|
||||
*/
|
||||
// BEGIN Custom Signal Code Block
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner) {
|
||||
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner, Consumer<Throwable> errorConsumer) {
|
||||
mCameraModule.bindToLifecycle(lifecycleOwner);
|
||||
this.errorConsumer = errorConsumer;
|
||||
if (pendingError != null) {
|
||||
errorConsumer.accept(pendingError);
|
||||
}
|
||||
}
|
||||
// END Custom Signal Code Block
|
||||
|
||||
|
||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
|
||||
mCameraModule = new SignalCameraXModule(this);
|
||||
|
||||
// Begin custom signal code block
|
||||
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
|
||||
mCameraModule = new SignalCameraXModule(this, error -> {
|
||||
if (errorConsumer != null) {
|
||||
errorConsumer.accept(error);
|
||||
} else {
|
||||
pendingError = error;
|
||||
}
|
||||
});
|
||||
// End custom signal code block
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
@@ -123,7 +124,9 @@ final class SignalCameraXModule {
|
||||
@Nullable
|
||||
ProcessCameraProvider mCameraProvider;
|
||||
|
||||
SignalCameraXModule(SignalCameraView view) {
|
||||
// BEGIN Custom Signal Code Block
|
||||
SignalCameraXModule(SignalCameraView view, Consumer<Throwable> errorConsumer) {
|
||||
// END Custom Signal Code Block
|
||||
mCameraView = view;
|
||||
|
||||
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
|
||||
@@ -141,7 +144,9 @@ final class SignalCameraXModule {
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
throw new RuntimeException("CameraX failed to initialize.", t);
|
||||
// BEGIN Custom Signal Code Block
|
||||
errorConsumer.accept(t);
|
||||
// END Custom Signal Code Block
|
||||
}
|
||||
}, CameraXExecutors.mainThreadExecutor());
|
||||
|
||||
@@ -222,17 +227,10 @@ final class SignalCameraXModule {
|
||||
// End Signal Custom Code Block
|
||||
|
||||
Rational targetAspectRatio;
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
|
||||
} else {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
}
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
// End Signal Custom Code Block
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||
|
||||
@@ -19,6 +19,6 @@ public final class AppCapabilities {
|
||||
* 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);
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, FeatureFlags.changeNumber());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -29,6 +28,7 @@ import androidx.multidex.MultiDexApplication;
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.AndroidLogger;
|
||||
@@ -125,7 +125,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
super.onCreate();
|
||||
|
||||
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
|
||||
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load(this))
|
||||
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load())
|
||||
.addBlocking("logging", () -> {
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
@@ -135,6 +135,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
|
||||
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
|
||||
})
|
||||
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
|
||||
.addBlocking("app-dependencies", this::initializeAppDependencies)
|
||||
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
|
||||
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
|
||||
@@ -171,6 +172,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
|
||||
@@ -86,7 +86,8 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode()
|
||||
: AppCompatDelegate.getDefaultNightMode();
|
||||
|
||||
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
|
||||
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
|
||||
configuration.orientation = Configuration.ORIENTATION_UNDEFINED;
|
||||
|
||||
applyOverrideConfiguration(configuration);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -12,7 +10,6 @@ import androidx.lifecycle.Observer;
|
||||
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
@@ -29,7 +26,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
@@ -49,11 +45,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
boolean pulseMention,
|
||||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
boolean canPlayInline,
|
||||
@NonNull Colorizer colorizer);
|
||||
|
||||
ConversationMessage getConversationMessage();
|
||||
@NonNull ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
@@ -71,7 +66,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms);
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
|
||||
@@ -92,6 +87,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onPlayInlineContent(ConversationMessage conversationMessage);
|
||||
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
|
||||
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
|
||||
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
@@ -50,6 +51,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
private static final int STATE_CREATE_SIGNAL_PIN = 7;
|
||||
private static final int STATE_TRANSFER_ONGOING = 8;
|
||||
private static final int STATE_TRANSFER_LOCKED = 9;
|
||||
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -153,6 +155,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
|
||||
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
|
||||
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -176,6 +179,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_TRANSFER_ONGOING;
|
||||
} else if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||
return STATE_TRANSFER_LOCKED;
|
||||
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
|
||||
return STATE_CHANGE_NUMBER_LOCK;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
@@ -243,6 +248,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return MainActivity.clearTop(this);
|
||||
}
|
||||
|
||||
private Intent getChangeNumberLockIntent() {
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
|
||||
@@ -43,32 +43,38 @@ 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.OvershootInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.widget.CompoundButton;
|
||||
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.AlertDialog;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
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 org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ShapeScrim;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -79,6 +85,7 @@ 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;
|
||||
@@ -109,13 +116,13 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
private static final String IDENTITY_EXTRA = "recipient_identity";
|
||||
private static final String VERIFIED_EXTRA = "verified_state";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
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 IdentityDatabase.IdentityRecord identityRecord)
|
||||
@NonNull IdentityRecord identityRecord)
|
||||
{
|
||||
return newIntent(context,
|
||||
identityRecord.getRecipientId(),
|
||||
@@ -124,7 +131,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityDatabase.IdentityRecord identityRecord,
|
||||
@NonNull IdentityRecord identityRecord,
|
||||
boolean verified)
|
||||
{
|
||||
return newIntent(context,
|
||||
@@ -154,9 +161,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
|
||||
@@ -214,7 +218,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||
public static class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener {
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
public static final String REMOTE_NUMBER = "remote_number";
|
||||
@@ -228,28 +232,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
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 TextView tapLabel;
|
||||
private TextSwitcher tapLabel;
|
||||
private TextView description;
|
||||
private View.OnClickListener clickListener;
|
||||
private SwitchCompat verified;
|
||||
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.verified = container.findViewById(R.id.verified_switch);
|
||||
this.verifyButton = container.findViewById(R.id.verify_button);
|
||||
this.qrVerified = container.findViewById(R.id.qr_verified);
|
||||
this.description = container.findViewById(R.id.description);
|
||||
this.tapLabel = container.findViewById(R.id.tap_label);
|
||||
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);
|
||||
@@ -263,15 +280,25 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
this.codes[10] = container.findViewById(R.id.code_eleventh);
|
||||
this.codes[11] = container.findViewById(R.id.code_twelth);
|
||||
|
||||
this.qrCode.setOnClickListener(clickListener);
|
||||
this.qrCodeContainer.setOnClickListener(clickListener);
|
||||
this.registerForContextMenu(numbersContainer);
|
||||
|
||||
this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
|
||||
this.verified.setOnCheckedChangeListener(this);
|
||||
updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false);
|
||||
this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true)));
|
||||
|
||||
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);
|
||||
@@ -327,6 +354,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Fingerprint fingerprint) {
|
||||
if (getActivity() == null) return;
|
||||
VerifyDisplayFragment.this.fingerprint = fingerprint;
|
||||
setFingerprintViews(fingerprint, true);
|
||||
getActivity().supportInvalidateOptionsMenu();
|
||||
@@ -353,6 +381,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
animateFailureOnDraw = false;
|
||||
animateVerifiedFailure();
|
||||
}
|
||||
|
||||
ThreadUtil.postToMain(this::onScrollChanged);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -410,9 +440,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,7 +512,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
private void setRecipientText(Recipient recipient) {
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
@@ -501,9 +533,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
if (animate) {
|
||||
ViewUtil.fadeIn(qrCode, 1000);
|
||||
ViewUtil.fadeIn(tapLabel, 1000);
|
||||
ViewUtil.fadeOut(loading, 300, View.GONE);
|
||||
} else {
|
||||
qrCode.setVisibility(View.VISIBLE);
|
||||
tapLabel.setVisibility(View.VISIBLE);
|
||||
loading.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +593,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__successful_match));
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
@@ -569,6 +605,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__failed_to_verify_safety_number));
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
@@ -576,7 +614,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
scaleAnimation.setInterpolator(new OvershootInterpolator());
|
||||
scaleAnimation.setInterpolator(new FastOutSlowInInterpolator());
|
||||
scaleAnimation.setDuration(800);
|
||||
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
@@ -594,6 +632,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
scaleAnimation.setInterpolator(new AnticipateInterpolator());
|
||||
scaleAnimation.setDuration(500);
|
||||
ViewUtil.animateOut(qrVerified, scaleAnimation, View.GONE);
|
||||
ViewUtil.fadeIn(qrCode, 800);
|
||||
qrCodeContainer.setEnabled(true);
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__tap_to_scan));
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
@@ -602,40 +643,70 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
|
||||
ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE);
|
||||
ViewUtil.animateIn(qrVerified, scaleAnimation);
|
||||
qrCodeContainer.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
|
||||
final Recipient recipient = this.recipient.get();
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
private void updateVerifyButton(boolean verified, boolean update) {
|
||||
currentVerifiedState = verified;
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (isChecked) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.saveIdentity(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED, false,
|
||||
System.currentTimeMillis(), true);
|
||||
} else {
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.setVerified(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.DEFAULT);
|
||||
if (verified) {
|
||||
verifyButton.setText(R.string.verify_display_fragment__clear_verification);
|
||||
} else {
|
||||
verifyButton.setText(R.string.verify_display_fragment__mark_as_verified);
|
||||
}
|
||||
|
||||
if (update) {
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (verified) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
ApplicationDependencies.getIdentityStore()
|
||||
.saveIdentityWithoutSideEffects(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
true);
|
||||
} else {
|
||||
ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, VerifiedStatus.DEFAULT);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
|
||||
remoteIdentity,
|
||||
verified ? VerifiedStatus.VERIFIED
|
||||
: VerifiedStatus.DEFAULT));
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), verified, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
|
||||
remoteIdentity,
|
||||
isChecked ? VerifiedStatus.VERIFIED
|
||||
: VerifiedStatus.DEFAULT));
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient, isChecked, false);
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,12 +714,23 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
private View container;
|
||||
private CameraView cameraView;
|
||||
private ShapeScrim cameraScrim;
|
||||
private ImageView cameraMarks;
|
||||
private ScanningThread scanningThread;
|
||||
private ScanListener scanListener;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = container.findViewById(R.id.scanner);
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = container.findViewById(R.id.scanner);
|
||||
this.cameraScrim = container.findViewById(R.id.camera_scrim);
|
||||
this.cameraMarks = container.findViewById(R.id.camera_marks);
|
||||
|
||||
OneShotPreDrawListener.add(cameraScrim, () -> {
|
||||
int width = cameraScrim.getScrimWidth();
|
||||
int height = cameraScrim.getScrimHeight();
|
||||
|
||||
ViewUtil.updateLayoutParams(cameraMarks, width, height);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -685,5 +767,4 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PictureInPictureParams;
|
||||
@@ -79,6 +77,7 @@ import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
@@ -86,6 +85,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(WebRtcCallActivity.class);
|
||||
@@ -366,15 +367,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void handleSetAudioHandset() {
|
||||
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(false);
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.EARPIECE);
|
||||
}
|
||||
|
||||
private void handleSetAudioSpeaker() {
|
||||
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(true);
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE);
|
||||
}
|
||||
|
||||
private void handleSetAudioBluetooth() {
|
||||
ApplicationDependencies.getSignalCallManager().setAudioBluetooth(true);
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.BLUETOOTH);
|
||||
}
|
||||
|
||||
private void handleSetMuteAudio(boolean enabled) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
|
||||
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
|
||||
private const val WIDTH = "signal.circleavatartransition.width"
|
||||
@@ -36,7 +35,7 @@ class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transitio
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
|
||||
if (view is AvatarImageView) {
|
||||
if (view.transitionName == "avatar") {
|
||||
val topLeft = intArrayOf(0, 0)
|
||||
view.getLocationOnScreen(topLeft)
|
||||
transitionValues.values[POSITION_ON_SCREEN] = topLeft
|
||||
@@ -51,7 +50,7 @@ class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transitio
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
if (view !is AvatarImageView || view.transitionName != "avatar") {
|
||||
if (view.transitionName != "avatar") {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,9 @@ object AvatarRenderer {
|
||||
avatar: Avatar.Text,
|
||||
inverted: Boolean = false,
|
||||
size: Int = DIMENSIONS,
|
||||
synchronous: Boolean = false
|
||||
): Drawable {
|
||||
return TextAvatarDrawable(context, avatar, inverted, size)
|
||||
return TextAvatarDrawable(context, avatar, inverted, size, synchronous)
|
||||
}
|
||||
|
||||
private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
@@ -66,7 +67,7 @@ object AvatarRenderer {
|
||||
|
||||
private fun renderText(context: Context, avatar: Avatar.Text, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
|
||||
val textDrawable = createTextDrawable(context, avatar)
|
||||
val textDrawable = createTextDrawable(context, avatar, synchronous = true)
|
||||
|
||||
canvas.drawColor(avatar.color.backgroundColor)
|
||||
textDrawable.draw(canvas)
|
||||
|
||||
@@ -3,52 +3,59 @@ package org.thoughtcrime.securesms.avatar
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import android.text.Layout
|
||||
import android.text.SpannableString
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import androidx.core.graphics.withTranslation
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider
|
||||
|
||||
/**
|
||||
* Uses EmojiTextView to properly render a Text Avatar with emoji in it.
|
||||
*/
|
||||
class TextAvatarDrawable(
|
||||
context: Context,
|
||||
avatar: Avatar.Text,
|
||||
private val context: Context,
|
||||
private val avatar: Avatar.Text,
|
||||
inverted: Boolean = false,
|
||||
private val size: Int = AvatarRenderer.DIMENSIONS,
|
||||
private val synchronous: Boolean = false
|
||||
) : Drawable() {
|
||||
|
||||
private val layout: FrameLayout = FrameLayout(context)
|
||||
private val textView: EmojiTextView = EmojiTextView(context)
|
||||
|
||||
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
|
||||
init {
|
||||
textView.typeface = AvatarRenderer.getTypeface(context)
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f))
|
||||
textView.text = avatar.text
|
||||
textView.gravity = Gravity.CENTER
|
||||
textView.setTextColor(if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor)
|
||||
textView.setForceCustomEmoji(true)
|
||||
textPaint.typeface = AvatarRenderer.getTypeface(context)
|
||||
textPaint.color = if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor
|
||||
textPaint.density = context.resources.displayMetrics.density
|
||||
|
||||
layout.addView(textView)
|
||||
|
||||
textView.updateLayoutParams {
|
||||
width = size
|
||||
height = size
|
||||
}
|
||||
|
||||
layout.measure(size, size)
|
||||
layout.layout(0, 0, size, size)
|
||||
setBounds(0, 0, size, size)
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int = size
|
||||
|
||||
override fun getIntrinsicWidth(): Int = size
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
layout.draw(canvas)
|
||||
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)
|
||||
}
|
||||
|
||||
if (newText == null) return
|
||||
|
||||
val layout = StaticLayout(SpannableString(newText), textPaint, width, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
|
||||
layout.draw(canvas, getStartX(layout), ((bounds.height() / 2) - ((layout.height / 2))).toFloat())
|
||||
}
|
||||
|
||||
private fun getStartX(layout: StaticLayout): Float {
|
||||
val direction = layout.getParagraphDirection(0)
|
||||
val lineWidth = layout.getLineWidth(0)
|
||||
val width = bounds.width()
|
||||
val xPos = (width - lineWidth) / 2
|
||||
return if (direction == Layout.DIR_LEFT_TO_RIGHT) xPos else -xPos
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
@@ -56,4 +63,10 @@ class TextAvatarDrawable(
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
|
||||
private fun Layout.draw(canvas: Canvas, x: Float, y: Float) {
|
||||
canvas.withTranslation(x, y) {
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,16 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelEditing() {
|
||||
Navigation.findNavController(requireView()).popBackStack()
|
||||
}
|
||||
|
||||
override fun onMainImageLoaded() {
|
||||
}
|
||||
|
||||
override fun onMainImageFailedToLoad() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY_EDIT = "org.thoughtcrime.securesms.avatar.photo.EDIT"
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
@@ -30,7 +31,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
|
||||
@@ -111,7 +111,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
putParcelable(SELECT_AVATAR_MEDIA, it)
|
||||
}
|
||||
)
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
|
||||
},
|
||||
{
|
||||
setFragmentResult(
|
||||
@@ -120,7 +120,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
putBoolean(SELECT_AVATAR_CLEAR, true)
|
||||
}
|
||||
)
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -147,9 +147,10 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
ViewUtil.hideKeyboard(requireContext(), requireView())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
val media: Media = Objects.requireNonNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
|
||||
val media: Media = requireNotNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
|
||||
viewModel.onAvatarPhotoSelectionCompleted(media)
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
@@ -195,23 +196,24 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
}
|
||||
}
|
||||
|
||||
fun openPhotoEditor(photo: Avatar.Photo) {
|
||||
private fun openPhotoEditor(photo: Avatar.Photo) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
|
||||
}
|
||||
|
||||
fun openVectorEditor(vector: Avatar.Vector) {
|
||||
private fun openVectorEditor(vector: Avatar.Vector) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
|
||||
}
|
||||
|
||||
fun openTextEditor(text: Avatar.Text?) {
|
||||
private fun openTextEditor(text: Avatar.Text?) {
|
||||
val bundle = if (text != null) AvatarBundler.bundleText(text) else null
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
|
||||
}
|
||||
|
||||
fun openCameraCapture() {
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openCameraCapture() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
@@ -226,7 +228,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
.execute()
|
||||
}
|
||||
|
||||
fun openGallery() {
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openGallery() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
|
||||
@@ -106,7 +106,7 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
}
|
||||
|
||||
textInput.setOnEditorActionListener { v, actionId, event ->
|
||||
textInput.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
tabLayout.getTabAt(1)?.select()
|
||||
true
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.documentfile.provider.DocumentFile;
|
||||
import com.annimon.stream.function.Predicate;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
|
||||
@@ -11,7 +11,7 @@ import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
@@ -162,9 +162,8 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
File dataFile = AttachmentDatabase.newFile(context);
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.thoughtcrime.securesms.badges
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Px
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.content.res.use
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges.insetWithOutline
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
private val TAG = Log.tag(BadgeImageView::class.java)
|
||||
|
||||
class BadgeImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : AppCompatImageView(context, attrs) {
|
||||
|
||||
@Px
|
||||
private var outlineWidth: Float = 0f
|
||||
|
||||
@ColorInt
|
||||
private var outlineColor: Int = Color.BLACK
|
||||
|
||||
init {
|
||||
context.obtainStyledAttributes(attrs, R.styleable.BadgeImageView).use {
|
||||
outlineWidth = it.getDimension(R.styleable.BadgeImageView_badge_outline_width, 0f)
|
||||
outlineColor = it.getColor(R.styleable.BadgeImageView_badge_outline_color, Color.BLACK)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBadgeFromRecipient(recipient: Recipient?) {
|
||||
if (recipient == null || recipient.badges.isEmpty()) {
|
||||
setBadge(null)
|
||||
} else {
|
||||
setBadge(recipient.badges[0])
|
||||
}
|
||||
}
|
||||
|
||||
fun setBadge(badge: Badge?) {
|
||||
visible = badge != null
|
||||
|
||||
val lifecycle = ViewUtil.getActivityLifecycle(this)
|
||||
if (lifecycle?.currentState == Lifecycle.State.DESTROYED) {
|
||||
Log.w(TAG, "Ignoring setBadge call for destroyed activity.")
|
||||
return
|
||||
}
|
||||
|
||||
GlideApp
|
||||
.with(this)
|
||||
.load(badge)
|
||||
.into(this)
|
||||
}
|
||||
|
||||
override fun setImageDrawable(drawable: Drawable?) {
|
||||
if (drawable == null || outlineWidth == 0f) {
|
||||
super.setImageDrawable(drawable)
|
||||
} else {
|
||||
super.setImageDrawable(
|
||||
drawable.insetWithOutline(
|
||||
outlineWidth, outlineColor
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.badges
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||
|
||||
class BadgeRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
fun setVisibilityForAllBadges(displayBadgesOnProfile: Boolean): Completable = Completable.fromAction {
|
||||
val badges = Recipient.self().badges.map { it.copy(visible = displayBadgesOnProfile) }
|
||||
ProfileUtil.uploadProfileWithBadges(context, badges)
|
||||
|
||||
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
recipientDatabase.setBadges(Recipient.self().id, badges)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
|
||||
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
|
||||
val badges = Recipient.self().badges
|
||||
val reOrderedBadges = listOf(featuredBadge) + (badges - featuredBadge)
|
||||
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
|
||||
|
||||
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.thoughtcrime.securesms.badges
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.graphics.withScale
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import com.google.android.flexbox.AlignItems
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeAnimator
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.util.customizeOnDraw
|
||||
|
||||
object Badges {
|
||||
fun Drawable.insetWithOutline(
|
||||
@Px outlineWidth: Float,
|
||||
@ColorInt outlineColor: Int
|
||||
): Drawable {
|
||||
val clone = mutate().constantState?.newDrawable()?.mutate()
|
||||
clone?.colorFilter = SimpleColorFilter(outlineColor)
|
||||
|
||||
return customizeOnDraw { wrapped, canvas ->
|
||||
clone?.bounds = wrapped.bounds
|
||||
clone?.draw(canvas)
|
||||
|
||||
val scale = 1 - ((outlineWidth * 2) / canvas.width)
|
||||
|
||||
canvas.withScale(x = scale, y = scale, canvas.width / 2f, canvas.height / 2f) {
|
||||
wrapped.draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Drawable.selectable(
|
||||
@Px outlineWidth: Float,
|
||||
@ColorInt outlineColor: Int,
|
||||
@ColorInt gapColor: Int,
|
||||
animator: BadgeAnimator
|
||||
): Drawable {
|
||||
val outline = mutate().constantState?.newDrawable()?.mutate()
|
||||
outline?.colorFilter = SimpleColorFilter(outlineColor)
|
||||
|
||||
val gap = mutate().constantState?.newDrawable()?.mutate()
|
||||
gap?.colorFilter = SimpleColorFilter(gapColor)
|
||||
|
||||
return customizeOnDraw { wrapped, canvas ->
|
||||
outline?.bounds = wrapped.bounds
|
||||
gap?.bounds = wrapped.bounds
|
||||
|
||||
outline?.draw(canvas)
|
||||
|
||||
val scale = 1 - ((outlineWidth * 2) / wrapped.bounds.width())
|
||||
val interpolatedScale = scale + (1f - scale) * animator.getFraction()
|
||||
|
||||
canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) {
|
||||
gap?.draw(canvas)
|
||||
|
||||
canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) {
|
||||
wrapped.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
if (animator.shouldInvalidate()) {
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DSLConfiguration.displayBadges(badges: List<Badge>, selectedBadge: Badge? = null) {
|
||||
badges
|
||||
.map { Badge.Model(it, it == selectedBadge) }
|
||||
.forEach { customPref(it) }
|
||||
|
||||
val empties = (4 - (badges.size % 4)) % 4
|
||||
repeat(empties) {
|
||||
customPref(Badge.EmptyModel())
|
||||
}
|
||||
}
|
||||
|
||||
fun createLayoutManagerForGridWithBadges(context: Context): RecyclerView.LayoutManager {
|
||||
val layoutManager = FlexboxLayoutManager(context)
|
||||
|
||||
layoutManager.flexDirection = FlexDirection.ROW
|
||||
layoutManager.alignItems = AlignItems.CENTER
|
||||
layoutManager.justifyContent = JustifyContent.CENTER
|
||||
|
||||
return layoutManager
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bumptech.glide.load.Key
|
||||
import com.bumptech.glide.request.target.CustomViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges.selectable
|
||||
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 java.security.MessageDigest
|
||||
|
||||
typealias OnBadgeClicked = (Badge, Boolean) -> Unit
|
||||
|
||||
/**
|
||||
* A Badge that can be collected and displayed by a user.
|
||||
*/
|
||||
data class Badge(
|
||||
val id: String,
|
||||
val category: Category,
|
||||
val imageUrl: Uri,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val expirationTimestamp: Long,
|
||||
val visible: Boolean
|
||||
) : Parcelable, Key {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
requireNotNull(parcel.readString()),
|
||||
Category.fromCode(requireNotNull(parcel.readString())),
|
||||
requireNotNull(parcel.readParcelable(Uri::class.java.classLoader)),
|
||||
requireNotNull(parcel.readString()),
|
||||
requireNotNull(parcel.readString()),
|
||||
parcel.readLong(),
|
||||
parcel.readByte() == 1.toByte()
|
||||
)
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(id)
|
||||
parcel.writeString(category.code)
|
||||
parcel.writeParcelable(imageUrl, flags)
|
||||
parcel.writeString(name)
|
||||
parcel.writeString(description)
|
||||
parcel.writeLong(expirationTimestamp)
|
||||
parcel.writeByte(if (visible) 1 else 0)
|
||||
}
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(id.toByteArray(Key.CHARSET))
|
||||
}
|
||||
|
||||
fun resolveDescription(shortName: String): String {
|
||||
return description.replace("{short_name}", shortName)
|
||||
}
|
||||
|
||||
class EmptyModel : PreferenceModel<EmptyModel>() {
|
||||
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
|
||||
}
|
||||
|
||||
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
|
||||
|
||||
private val name: TextView = itemView.findViewById(R.id.name)
|
||||
|
||||
init {
|
||||
itemView.isEnabled = false
|
||||
itemView.isFocusable = false
|
||||
itemView.isClickable = false
|
||||
itemView.visibility = View.INVISIBLE
|
||||
|
||||
name.text = " "
|
||||
}
|
||||
|
||||
override fun bind(model: EmptyModel) = Unit
|
||||
}
|
||||
|
||||
class Model(
|
||||
val badge: Badge,
|
||||
val isSelected: Boolean = false
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.badge.id == badge.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) && badge == newItem.badge && isSelected == newItem.isSelected
|
||||
}
|
||||
|
||||
override fun getChangePayload(newItem: Model): Any? {
|
||||
return if (badge == newItem.badge && isSelected != newItem.isSelected) {
|
||||
SELECTION_CHANGED
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, private val onBadgeClicked: OnBadgeClicked) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
||||
private val name: TextView = itemView.findViewById(R.id.name)
|
||||
private val target = Target(badge)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.setOnClickListener {
|
||||
onBadgeClicked(model.badge, model.isSelected)
|
||||
}
|
||||
|
||||
if (payload.isNotEmpty()) {
|
||||
if (model.isSelected) {
|
||||
target.animateToStart()
|
||||
} else {
|
||||
target.animateToEnd()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
GlideApp.with(badge)
|
||||
.load(model.badge)
|
||||
.into(target)
|
||||
|
||||
if (model.isSelected) {
|
||||
target.setAnimationToStart()
|
||||
} else {
|
||||
target.setAnimationToEnd()
|
||||
}
|
||||
|
||||
name.text = model.badge.name
|
||||
}
|
||||
}
|
||||
|
||||
enum class Category(val code: String) {
|
||||
Donor("donor"),
|
||||
Other("other"),
|
||||
Testing("testing"); // Will be removed before final release
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String): Category {
|
||||
return when (code) {
|
||||
"donor" -> Donor
|
||||
"testing" -> Testing
|
||||
else -> Other
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Target(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
|
||||
|
||||
private val animator: BadgeAnimator = BadgeAnimator()
|
||||
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
view.setImageDrawable(errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
val drawable = resource.selectable(
|
||||
DimensionUnit.DP.toPixels(2.5f),
|
||||
ContextCompat.getColor(view.context, R.color.signal_inverse_primary),
|
||||
ContextCompat.getColor(view.context, R.color.signal_background_primary),
|
||||
animator
|
||||
)
|
||||
|
||||
view.setImageDrawable(drawable)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(placeholder: Drawable?) {
|
||||
view.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
fun setAnimationToStart() {
|
||||
animator.setState(BadgeAnimator.State.START)
|
||||
view.drawable?.invalidateSelf()
|
||||
}
|
||||
|
||||
fun setAnimationToEnd() {
|
||||
animator.setState(BadgeAnimator.State.END)
|
||||
view.drawable?.invalidateSelf()
|
||||
}
|
||||
|
||||
fun animateToStart() {
|
||||
animator.setState(BadgeAnimator.State.REVERSE)
|
||||
view.drawable?.invalidateSelf()
|
||||
}
|
||||
|
||||
fun animateToEnd() {
|
||||
animator.setState(BadgeAnimator.State.FORWARD)
|
||||
view.drawable?.invalidateSelf()
|
||||
}
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Badge> {
|
||||
private val SELECTION_CHANGED = Any()
|
||||
|
||||
override fun createFromParcel(parcel: Parcel): Badge {
|
||||
return Badge(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Badge?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
class BadgeAnimator {
|
||||
|
||||
val duration = 250L
|
||||
|
||||
var state: State = State.START
|
||||
private set
|
||||
|
||||
private var startTime: Long = 0L
|
||||
|
||||
fun getFraction(): Float {
|
||||
return when (state) {
|
||||
State.START -> 0f
|
||||
State.END -> 1f
|
||||
State.FORWARD -> Util.clamp((System.currentTimeMillis() - startTime) / duration.toFloat(), 0f, 1f)
|
||||
State.REVERSE -> 1f - Util.clamp((System.currentTimeMillis() - startTime) / duration.toFloat(), 0f, 1f)
|
||||
}
|
||||
}
|
||||
|
||||
fun setState(newState: State) {
|
||||
shouldInvalidate()
|
||||
|
||||
if (state == newState) {
|
||||
return
|
||||
}
|
||||
|
||||
if (newState == State.END || newState == State.START) {
|
||||
state = newState
|
||||
startTime = 0L
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.START && newState == State.REVERSE) {
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.END && newState == State.FORWARD) {
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.START && newState == State.FORWARD) {
|
||||
state = State.FORWARD
|
||||
startTime = System.currentTimeMillis()
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.END && newState == State.REVERSE) {
|
||||
state = State.REVERSE
|
||||
startTime = System.currentTimeMillis()
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.FORWARD && newState == State.REVERSE) {
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
val delta = duration - elapsed
|
||||
startTime -= delta
|
||||
state = State.REVERSE
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.REVERSE && newState == State.FORWARD) {
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
val delta = duration - elapsed
|
||||
startTime -= delta
|
||||
state = State.FORWARD
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldInvalidate(): Boolean {
|
||||
if (state == State.START || state == State.END) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (state == State.FORWARD && getFraction() == 1f) {
|
||||
state = State.END
|
||||
return false
|
||||
}
|
||||
|
||||
if (state == State.REVERSE && getFraction() == 0f) {
|
||||
state = State.START
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
enum class State {
|
||||
START,
|
||||
FORWARD,
|
||||
REVERSE,
|
||||
END
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bumptech.glide.request.target.CustomViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges.insetWithOutline
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
object FeaturedBadgePreview {
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
|
||||
}
|
||||
|
||||
data class Model(val badge: Badge?) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.badge?.id == badge?.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) && badge == newItem.badge
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
|
||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
||||
private val target: Target = Target(badge)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
avatar.setRecipient(Recipient.self())
|
||||
avatar.disableQuickContact()
|
||||
|
||||
if (model.badge != null) {
|
||||
GlideApp.with(badge)
|
||||
.load(model.badge)
|
||||
.into(target)
|
||||
} else {
|
||||
GlideApp.with(badge).clear(badge)
|
||||
badge.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Target(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
view.setImageDrawable(errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
view.setImageDrawable(
|
||||
resource.insetWithOutline(
|
||||
DimensionUnit.DP.toPixels(2.5f),
|
||||
ContextCompat.getColor(view.context, R.color.signal_background_primary)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(placeholder: Drawable?) {
|
||||
view.setImageDrawable(placeholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
data class LargeBadge(
|
||||
val badge: Badge
|
||||
) {
|
||||
|
||||
class Model(val largeBadge: LargeBadge, val shortName: String) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.largeBadge.badge.id == largeBadge.badge.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return newItem.largeBadge == largeBadge && newItem.shortName == shortName
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyModel : MappingModel<EmptyModel> {
|
||||
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: EmptyModel): Boolean = true
|
||||
}
|
||||
|
||||
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
|
||||
override fun bind(model: EmptyModel) {
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
||||
private val name: TextView = itemView.findViewById(R.id.name)
|
||||
private val description: TextView = itemView.findViewById(R.id.description)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
GlideApp.with(badge)
|
||||
.load(model.largeBadge.badge)
|
||||
.into(badge)
|
||||
|
||||
name.text = model.largeBadge.badge.name
|
||||
description.text = model.largeBadge.badge.resolveDescription(model.shortName)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
enum class SelectFeaturedBadgeEvent {
|
||||
NO_BADGE_SELECTED,
|
||||
FAILED_TO_UPDATE_PROFILE,
|
||||
SAVE_SUCCESSFUL
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.FeaturedBadgePreview
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
/**
|
||||
* Fragment which allows user to select one of their badges to be their "Featured" badge.
|
||||
*/
|
||||
class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
||||
titleId = R.string.BadgesOverviewFragment__featured_badge,
|
||||
layoutId = R.layout.select_featured_badge_fragment,
|
||||
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
|
||||
) {
|
||||
|
||||
private val viewModel: SelectFeaturedBadgeViewModel by viewModels(factoryProducer = { SelectFeaturedBadgeViewModel.Factory(BadgeRepository(requireContext())) })
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var scrollShadow: View
|
||||
private lateinit var save: View
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
scrollShadow = view.findViewById(R.id.scroll_shadow)
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
save = view.findViewById(R.id.save)
|
||||
save.setOnClickListener {
|
||||
viewModel.save()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ToolbarShadowAnimationHelper(scrollShadow)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, isSelected ->
|
||||
if (!isSelected) {
|
||||
viewModel.setSelectedBadge(badge)
|
||||
}
|
||||
}
|
||||
|
||||
val previewView: View = requireView().findViewById(R.id.preview)
|
||||
val previewViewHolder = FeaturedBadgePreview.ViewHolder(previewView)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
lifecycleDisposable += viewModel.events.subscribe { event: SelectFeaturedBadgeEvent ->
|
||||
when (event) {
|
||||
SelectFeaturedBadgeEvent.NO_BADGE_SELECTED -> Toast.makeText(requireContext(), R.string.SelectFeaturedBadgeFragment__you_must_select_a_badge, Toast.LENGTH_LONG).show()
|
||||
SelectFeaturedBadgeEvent.FAILED_TO_UPDATE_PROFILE -> Toast.makeText(requireContext(), R.string.SelectFeaturedBadgeFragment__failed_to_update_profile, Toast.LENGTH_LONG).show()
|
||||
SelectFeaturedBadgeEvent.SAVE_SUCCESSFUL -> findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
save.isEnabled = state.stage == SelectFeaturedBadgeState.Stage.READY
|
||||
previewViewHolder.bind(FeaturedBadgePreview.Model(state.selectedBadge))
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: SelectFeaturedBadgeState): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.SelectFeaturedBadgeFragment__select_a_badge)
|
||||
displayBadges(state.allUnlockedBadges, state.selectedBadge)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class SelectFeaturedBadgeState(
|
||||
val stage: Stage = Stage.INIT,
|
||||
val selectedBadge: Badge? = null,
|
||||
val allUnlockedBadges: List<Badge> = listOf()
|
||||
) {
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
SAVING
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
private val TAG = Log.tag(SelectFeaturedBadgeViewModel::class.java)
|
||||
|
||||
class SelectFeaturedBadgeViewModel(private val repository: BadgeRepository) : ViewModel() {
|
||||
|
||||
private val store = Store(SelectFeaturedBadgeState())
|
||||
private val eventSubject = PublishSubject.create<SelectFeaturedBadgeEvent>()
|
||||
|
||||
val state: LiveData<SelectFeaturedBadgeState> = store.stateLiveData
|
||||
val events: Observable<SelectFeaturedBadgeEvent> = eventSubject.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
|
||||
state.copy(
|
||||
stage = if (state.stage == SelectFeaturedBadgeState.Stage.INIT) SelectFeaturedBadgeState.Stage.READY else state.stage,
|
||||
selectedBadge = recipient.badges.firstOrNull(),
|
||||
allUnlockedBadges = recipient.badges
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedBadge(badge: Badge) {
|
||||
store.update { it.copy(selectedBadge = badge) }
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.selectedBadge == null) {
|
||||
eventSubject.onNext(SelectFeaturedBadgeEvent.NO_BADGE_SELECTED)
|
||||
return
|
||||
}
|
||||
|
||||
store.update { it.copy(stage = SelectFeaturedBadgeState.Stage.SAVING) }
|
||||
disposables += repository.setFeaturedBadge(snapshot.selectedBadge).subscribeBy(
|
||||
onComplete = {
|
||||
eventSubject.onNext(SelectFeaturedBadgeEvent.SAVE_SUCCESSFUL)
|
||||
},
|
||||
onError = { error ->
|
||||
Log.e(TAG, "Failed to update profile.", error)
|
||||
eventSubject.onNext(SelectFeaturedBadgeEvent.FAILED_TO_UPDATE_PROFILE)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(private val badgeRepository: BadgeRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(SelectFeaturedBadgeViewModel(badgeRepository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
enum class BadgesOverviewEvent {
|
||||
FAILED_TO_UPDATE_PROFILE
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
/**
|
||||
* Fragment to allow user to manage options related to the badges they've unlocked.
|
||||
*/
|
||||
class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ManageProfileFragment_badges,
|
||||
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
|
||||
) {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val viewModel: BadgesOverviewViewModel by viewModels(factoryProducer = { BadgesOverviewViewModel.Factory(BadgeRepository(requireContext())) })
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, _ ->
|
||||
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
|
||||
lifecycleDisposable.add(
|
||||
viewModel.events.subscribe { event: BadgesOverviewEvent ->
|
||||
when (event) {
|
||||
BadgesOverviewEvent.FAILED_TO_UPDATE_PROFILE -> Toast.makeText(requireContext(), R.string.BadgesOverviewFragment__failed_to_update_profile, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: BadgesOverviewState): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.BadgesOverviewFragment__my_badges)
|
||||
|
||||
displayBadges(state.allUnlockedBadges)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile),
|
||||
isChecked = state.displayBadgesOnProfile,
|
||||
onClick = {
|
||||
viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__featured_badge),
|
||||
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY,
|
||||
onClick = {
|
||||
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class BadgesOverviewState(
|
||||
val stage: Stage = Stage.INIT,
|
||||
val allUnlockedBadges: List<Badge> = listOf(),
|
||||
val featuredBadge: Badge? = null,
|
||||
val displayBadgesOnProfile: Boolean = false
|
||||
) {
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
UPDATING
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
||||
|
||||
class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : ViewModel() {
|
||||
private val store = Store(BadgesOverviewState())
|
||||
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
|
||||
|
||||
val state: LiveData<BadgesOverviewState> = store.stateLiveData
|
||||
val events: Observable<BadgesOverviewEvent> = eventSubject.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
|
||||
state.copy(
|
||||
stage = if (state.stage == BadgesOverviewState.Stage.INIT) BadgesOverviewState.Stage.READY else state.stage,
|
||||
allUnlockedBadges = recipient.badges,
|
||||
displayBadgesOnProfile = recipient.badges.firstOrNull()?.visible == true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDisplayBadgesOnProfile(displayBadgesOnProfile: Boolean) {
|
||||
disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile)
|
||||
.subscribe(
|
||||
{
|
||||
store.update { it.copy(stage = BadgesOverviewState.Stage.READY) }
|
||||
},
|
||||
{ error ->
|
||||
Log.e(TAG, "Failed to update visibility.", error)
|
||||
store.update { it.copy(stage = BadgesOverviewState.Stage.READY) }
|
||||
eventSubject.onNext(BadgesOverviewEvent.FAILED_TO_UPDATE_PROFILE)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(private val badgeRepository: BadgeRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
private val viewModel: ViewBadgeViewModel by viewModels(factoryProducer = { ViewBadgeViewModel.Factory(getStartBadge(), getRecipientId(), BadgeRepository(requireContext())) })
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.view_badge_bottom_sheet_dialog_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
postponeEnterTransition()
|
||||
|
||||
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
||||
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
|
||||
val action: MaterialButton = view.findViewById(R.id.action)
|
||||
|
||||
if (getRecipientId() == Recipient.self().id) {
|
||||
action.visible = false
|
||||
}
|
||||
|
||||
val adapter = MappingAdapter()
|
||||
|
||||
LargeBadge.register(adapter)
|
||||
pager.adapter = adapter
|
||||
adapter.submitList(listOf(LargeBadge.EmptyModel()))
|
||||
|
||||
TabLayoutMediator(tabs, pager) { _, _ ->
|
||||
}.attach()
|
||||
|
||||
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
if (adapter.getModel(position).map { it is LargeBadge.Model }.orElse(false)) {
|
||||
viewModel.onPageSelected(position)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
if (state.recipient == null || state.badgeLoadState == ViewBadgeState.LoadState.INIT) {
|
||||
return@observe
|
||||
}
|
||||
|
||||
if (state.allBadgesVisibleOnProfile.isEmpty()) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
adapter.submitList(
|
||||
state.allBadgesVisibleOnProfile.map {
|
||||
LargeBadge.Model(LargeBadge(it), state.recipient.getShortDisplayName(requireContext()))
|
||||
}
|
||||
) {
|
||||
val stateSelectedIndex = state.allBadgesVisibleOnProfile.indexOf(state.selectedBadge)
|
||||
if (state.selectedBadge != null && pager.currentItem != stateSelectedIndex) {
|
||||
pager.currentItem = stateSelectedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStartBadge(): Badge? = requireArguments().getParcelable(ARG_START_BADGE)
|
||||
|
||||
private fun getRecipientId(): RecipientId = requireNotNull(requireArguments().getParcelable(ARG_RECIPIENT_ID))
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_START_BADGE = "start_badge"
|
||||
private const val ARG_RECIPIENT_ID = "recipient_id"
|
||||
|
||||
@JvmStatic
|
||||
fun show(
|
||||
fragmentManager: FragmentManager,
|
||||
recipientId: RecipientId,
|
||||
startBadge: Badge? = null
|
||||
) {
|
||||
ViewBadgeBottomSheetDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_START_BADGE, startBadge)
|
||||
putParcelable(ARG_RECIPIENT_ID, recipientId)
|
||||
}
|
||||
|
||||
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ViewBadgeState(
|
||||
val allBadgesVisibleOnProfile: List<Badge> = listOf(),
|
||||
val badgeLoadState: LoadState = LoadState.INIT,
|
||||
val selectedBadge: Badge? = null,
|
||||
val recipient: Recipient? = null
|
||||
) {
|
||||
enum class LoadState {
|
||||
INIT,
|
||||
LOADED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class ViewBadgeViewModel(
|
||||
private val startBadge: Badge?,
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: BadgeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val store = Store(ViewBadgeState())
|
||||
|
||||
val state: LiveData<ViewBadgeState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(recipientId).liveData) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
allBadgesVisibleOnProfile = recipient.badges,
|
||||
selectedBadge = startBadge ?: recipient.badges.firstOrNull(),
|
||||
badgeLoadState = ViewBadgeState.LoadState.LOADED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun onPageSelected(position: Int) {
|
||||
if (position > store.state.allBadgesVisibleOnProfile.size - 1 || position < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(selectedBadge = it.allBadgesVisibleOnProfile[position])
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val startBadge: Badge?,
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: BadgeRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(ViewBadgeViewModel(startBadge, recipientId, repository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -207,8 +208,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
this.chatColors = chatColors;
|
||||
recipientContactPhoto = photo;
|
||||
|
||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
|
||||
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
|
||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider, ViewUtil.getWidth(this))
|
||||
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider, ViewUtil.getWidth(this));
|
||||
|
||||
if (fixedSizeTarget != null) {
|
||||
requestManager.clear(fixedSizeTarget);
|
||||
|
||||
@@ -5,10 +5,14 @@ import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -17,15 +21,20 @@ import androidx.core.content.ContextCompat;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class FromTextView extends EmojiTextView {
|
||||
public class FromTextView extends SimpleEmojiTextView {
|
||||
|
||||
private static final String TAG = Log.tag(FromTextView.class);
|
||||
|
||||
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
||||
|
||||
public FromTextView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -45,20 +54,9 @@ public class FromTextView extends EmojiTextView {
|
||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||
String fromString = recipient.getDisplayName(getContext());
|
||||
|
||||
int typeface;
|
||||
|
||||
if (!read) {
|
||||
typeface = Typeface.BOLD;
|
||||
} else {
|
||||
typeface = Typeface.NORMAL;
|
||||
}
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
|
||||
SpannableString fromSpan = new SpannableString(fromString);
|
||||
fromSpan.setSpan(new StyleSpan(typeface), 0, builder.length(),
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
SpannableString fromSpan = new SpannableString(fromString);
|
||||
fromSpan.setSpan(getFontSpan(!read), 0, fromSpan.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
if (recipient.isSelf()) {
|
||||
builder.append(getContext().getString(R.string.note_to_self));
|
||||
@@ -85,4 +83,8 @@ public class FromTextView extends EmojiTextView {
|
||||
|
||||
return mutedDrawable;
|
||||
}
|
||||
|
||||
private CharacterStyle getFontSpan(boolean isBold) {
|
||||
return isBold ? SpanUtil.getBoldSpan() : SpanUtil.getNormalSpan();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Fullscreen Dialog Fragment which will dismiss itself when the keyboard is closed
|
||||
*/
|
||||
abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
DialogFragment(contentLayoutId),
|
||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||
KeyboardAwareLinearLayout.OnKeyboardHiddenListener {
|
||||
|
||||
private var hasShown = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setStyle(STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
|
||||
dialog.window?.setDimAmount(0f)
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
hasShown = false
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
return if (view is KeyboardAwareLinearLayout) {
|
||||
view.addOnKeyboardShownListener(this)
|
||||
view.addOnKeyboardHiddenListener(this)
|
||||
view
|
||||
} else {
|
||||
throw IllegalStateException("Expected parent of view hierarchy to be keyboard aware.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyboardShown() {
|
||||
hasShown = true
|
||||
}
|
||||
|
||||
override fun onKeyboardHidden() {
|
||||
if (hasShown) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class MaskView extends View {
|
||||
|
||||
private MaskTarget maskTarget;
|
||||
private ViewGroup activityContentView;
|
||||
private Paint maskPaint;
|
||||
private Rect drawingRect = new Rect();
|
||||
private float targetParentTranslationY;
|
||||
|
||||
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
|
||||
|
||||
public MaskView(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MaskView(@NonNull Context context, @Nullable AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
setLayerType(LAYER_TYPE_HARDWARE, maskPaint);
|
||||
|
||||
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
activityContentView = getRootView().findViewById(android.R.id.content);
|
||||
}
|
||||
|
||||
public void setTarget(@Nullable MaskTarget maskTarget) {
|
||||
if (this.maskTarget != null) {
|
||||
removeOnDrawListener(this.maskTarget, onDrawListener);
|
||||
}
|
||||
|
||||
this.maskTarget = maskTarget;
|
||||
|
||||
if (this.maskTarget != null) {
|
||||
addOnDrawListener(maskTarget, onDrawListener);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setTargetParentTranslationY(float targetParentTranslationY) {
|
||||
this.targetParentTranslationY = targetParentTranslationY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (nothingToMask(maskTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
maskTarget.getPrimaryTarget().getDrawingRect(drawingRect);
|
||||
activityContentView.offsetDescendantRectToMyCoords(maskTarget.getPrimaryTarget(), drawingRect);
|
||||
|
||||
drawingRect.top += targetParentTranslationY;
|
||||
drawingRect.bottom += targetParentTranslationY;
|
||||
|
||||
Bitmap mask = Bitmap.createBitmap(maskTarget.getPrimaryTarget().getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
|
||||
Canvas maskCanvas = new Canvas(mask);
|
||||
|
||||
maskTarget.draw(maskCanvas);
|
||||
|
||||
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
||||
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) maskTarget.getPrimaryTarget().getLayoutParams();
|
||||
canvas.drawBitmap(mask, params.leftMargin, drawingRect.top, maskPaint);
|
||||
|
||||
mask.recycle();
|
||||
}
|
||||
|
||||
private static void removeOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) {
|
||||
for (View view : maskTarget.getAllTargets()) {
|
||||
if (view != null) {
|
||||
view.getViewTreeObserver().removeOnDrawListener(onDrawListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) {
|
||||
for (View view : maskTarget.getAllTargets()) {
|
||||
if (view != null) {
|
||||
view.getViewTreeObserver().addOnDrawListener(onDrawListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean nothingToMask(@Nullable MaskTarget maskTarget) {
|
||||
if (maskTarget == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (View view : maskTarget.getAllTargets()) {
|
||||
if (view == null || !view.isAttachedToWindow()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class MaskTarget {
|
||||
|
||||
private final View primaryTarget;
|
||||
|
||||
public MaskTarget(@NonNull View primaryTarget) {
|
||||
this.primaryTarget = primaryTarget;
|
||||
}
|
||||
|
||||
final @NonNull View getPrimaryTarget() {
|
||||
return primaryTarget;
|
||||
}
|
||||
|
||||
protected @NonNull List<View> getAllTargets() {
|
||||
return Collections.singletonList(primaryTarget);
|
||||
}
|
||||
|
||||
protected void draw(@NonNull Canvas canvas) {
|
||||
primaryTarget.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
@@ -228,55 +229,63 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
bodyView.setVisibility(GONE);
|
||||
mediaDescriptionText.setVisibility(VISIBLE);
|
||||
|
||||
List<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
|
||||
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
|
||||
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
|
||||
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
|
||||
List<Slide> stickerSlides = Stream.of(attachments.getSlides()).filter(Slide::hasSticker).limit(1).toList();
|
||||
List<Slide> viewOnceSlides = Stream.of(attachments.getSlides()).filter(Slide::hasViewOnce).limit(1).toList();
|
||||
Slide audioSlide = attachments.getSlides().stream().filter(Slide::hasAudio).findFirst().orElse(null);
|
||||
Slide documentSlide = attachments.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide imageSlide = attachments.getSlides().stream().filter(Slide::hasImage).findFirst().orElse(null);
|
||||
Slide videoSlide = attachments.getSlides().stream().filter(Slide::hasVideo).findFirst().orElse(null);
|
||||
Slide stickerSlide = attachments.getSlides().stream().filter(Slide::hasSticker).findFirst().orElse(null);
|
||||
Slide viewOnceSlide = attachments.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
|
||||
|
||||
// Given that most types have images, we specifically check images last
|
||||
if (!viewOnceSlides.isEmpty()) {
|
||||
if (viewOnceSlide != null) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_view_once_media);
|
||||
} else if (!audioSlides.isEmpty()) {
|
||||
} else if (audioSlide != null) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_audio);
|
||||
} else if (!documentSlides.isEmpty()) {
|
||||
} else if (documentSlide != null) {
|
||||
mediaDescriptionText.setVisibility(GONE);
|
||||
} else if (!videoSlides.isEmpty()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_video);
|
||||
} else if (!stickerSlides.isEmpty()) {
|
||||
} else if (videoSlide != null) {
|
||||
if (videoSlide.isVideoGif()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_gif);
|
||||
} else {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_video);
|
||||
}
|
||||
} else if (stickerSlide != null) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_sticker);
|
||||
} else if (!imageSlides.isEmpty()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_photo);
|
||||
} else if (imageSlide != null) {
|
||||
if (MediaUtil.isGif(imageSlide.getContentType())) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_gif);
|
||||
} else {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_photo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
|
||||
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).limit(1).toList();
|
||||
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
|
||||
List<Slide> viewOnceSlides = Stream.of(attachments.getSlides()).filter(Slide::hasViewOnce).limit(1).toList();
|
||||
Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null);
|
||||
Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
|
||||
|
||||
attachmentVideoOverlayView.setVisibility(GONE);
|
||||
|
||||
if (!viewOnceSlides.isEmpty()) {
|
||||
if (viewOnceSlide != null) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
} else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getUri() != null) {
|
||||
} else if (imageVideoSlide != null && imageVideoSlide.getUri() != null) {
|
||||
thumbnailView.setVisibility(VISIBLE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
dismissView.setBackgroundResource(R.drawable.dismiss_background);
|
||||
if (imageVideoSlides.get(0).hasVideo()) {
|
||||
if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) {
|
||||
attachmentVideoOverlayView.setVisibility(VISIBLE);
|
||||
}
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getUri()))
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlide.getUri()))
|
||||
.centerCrop()
|
||||
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
} else if (!documentSlides.isEmpty()){
|
||||
} else if (documentSlide != null){
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(VISIBLE);
|
||||
attachmentNameView.setText(documentSlides.get(0).getFileName().or(""));
|
||||
attachmentNameView.setText(documentSlide.getFileName().or(""));
|
||||
} else {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Xfermode;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -100,6 +101,10 @@ public final class RotatableGradientDrawable extends Drawable {
|
||||
fillPaint.setShader(new LinearGradient(fillRect.left, fillRect.top, fillRect.right, fillRect.bottom, colors, positions, Shader.TileMode.CLAMP));
|
||||
}
|
||||
|
||||
public void setXfermode(@NonNull Xfermode xfermode) {
|
||||
fillPaint.setXfermode(xfermode);
|
||||
}
|
||||
|
||||
private static Point cornerPrime(@NonNull Point origin, @NonNull Point corner, float degrees) {
|
||||
return new Point(xPrime(origin, corner, Math.toRadians(degrees)), yPrime(origin, corner, Math.toRadians(degrees)));
|
||||
}
|
||||
|
||||
@@ -23,9 +23,12 @@ public class ShapeScrim extends View {
|
||||
private final Paint eraser;
|
||||
private final ShapeType shape;
|
||||
private final float radius;
|
||||
private final int canvasColor;
|
||||
|
||||
private Bitmap scrim;
|
||||
private Canvas scrimCanvas;
|
||||
private int scrimWidth;
|
||||
private int scrimHeight;
|
||||
|
||||
public ShapeScrim(Context context) {
|
||||
this(context, null);
|
||||
@@ -57,13 +60,30 @@ public class ShapeScrim extends View {
|
||||
this.eraser = new Paint();
|
||||
this.eraser.setColor(0xFFFFFFFF);
|
||||
this.eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
this.canvasColor = Color.parseColor("#55BDBDBD");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
int shortDimension = Math.min(getWidth(), getHeight());
|
||||
float drawRadius = shortDimension * radius;
|
||||
|
||||
float left = (getMeasuredWidth() / 2 ) - drawRadius;
|
||||
float top = (getMeasuredHeight() / 2) - drawRadius;
|
||||
float right = left + (drawRadius * 2);
|
||||
float bottom = top + (drawRadius * 2);
|
||||
|
||||
scrimWidth = (int) (right - left);
|
||||
scrimHeight = (int) (bottom - top);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
int shortDimension = getWidth() < getHeight() ? getWidth() : getHeight();
|
||||
int shortDimension = Math.min(getWidth(), getHeight());
|
||||
float drawRadius = shortDimension * radius;
|
||||
|
||||
if (scrimCanvas == null) {
|
||||
@@ -72,7 +92,7 @@ public class ShapeScrim extends View {
|
||||
}
|
||||
|
||||
scrim.eraseColor(Color.TRANSPARENT);
|
||||
scrimCanvas.drawColor(Color.parseColor("#55BDBDBD"));
|
||||
scrimCanvas.drawColor(canvasColor);
|
||||
|
||||
if (shape == ShapeType.CIRCLE) drawCircle(scrimCanvas, drawRadius, eraser);
|
||||
else drawSquare(scrimCanvas, drawRadius, eraser);
|
||||
@@ -104,4 +124,12 @@ public class ShapeScrim extends View {
|
||||
|
||||
canvas.drawRoundRect(square, 25, 25, eraser);
|
||||
}
|
||||
|
||||
public int getScrimWidth() {
|
||||
return scrimWidth;
|
||||
}
|
||||
|
||||
public int getScrimHeight() {
|
||||
return scrimHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.drawable.shapes.Shape;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -438,8 +439,10 @@ public class ThumbnailView extends FrameLayout {
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.transition(withCrossFade()), fit);
|
||||
|
||||
if (slide.isInProgress()) return request;
|
||||
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
|
||||
|
||||
if (slide.isInProgress() || doNotShowMissingThumbnailImage) return request;
|
||||
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
}
|
||||
|
||||
private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
|
||||
@@ -66,6 +66,7 @@ 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);
|
||||
|
||||
@@ -26,13 +26,15 @@ 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;
|
||||
|
||||
class EmojiProvider {
|
||||
public class EmojiProvider {
|
||||
|
||||
private static final String TAG = Log.tag(EmojiProvider.class);
|
||||
private static final Paint PAINT = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
static @Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) {
|
||||
public static @Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) {
|
||||
if (text == null) return null;
|
||||
return new EmojiParser(EmojiSource.getLatest().getEmojiTree()).findCandidates(text);
|
||||
}
|
||||
@@ -64,6 +66,32 @@ class EmojiProvider {
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static @Nullable Spannable emojify(@NonNull Context context,
|
||||
@Nullable EmojiParser.CandidateList matches,
|
||||
@Nullable CharSequence text,
|
||||
@NonNull Paint paint,
|
||||
boolean synchronous)
|
||||
{
|
||||
if (matches == null || text == null) return null;
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||
|
||||
for (EmojiParser.Candidate candidate : matches) {
|
||||
Drawable drawable;
|
||||
if (synchronous) {
|
||||
drawable = getEmojiDrawableSync(context, candidate.getDrawInfo());
|
||||
} else {
|
||||
drawable = getEmojiDrawable(context, candidate.getDrawInfo(), null);
|
||||
}
|
||||
|
||||
if (drawable != null) {
|
||||
builder.setSpan(new EmojiSpan(context, drawable, paint), candidate.getStartIndex(), candidate.getEndIndex(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji) {
|
||||
EmojiDrawInfo drawInfo = EmojiSource.getLatest().getEmojiTree().getEmoji(emoji, 0, emoji.length());
|
||||
return getEmojiDrawable(context, drawInfo, null);
|
||||
@@ -113,6 +141,43 @@ class EmojiProvider {
|
||||
return drawable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an EmojiDrawable from the Page Cache synchronously
|
||||
*
|
||||
* @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) {
|
||||
ThreadUtil.assertNotMainThread();
|
||||
if (drawInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int lowMemoryDecodeScale = DeviceProperties.isLowMemoryDevice(context) ? 2 : 1;
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static final class EmojiDrawable extends Drawable {
|
||||
private final float intrinsicWidth;
|
||||
private final float intrinsicHeight;
|
||||
@@ -160,7 +225,6 @@ class EmojiProvider {
|
||||
}
|
||||
|
||||
public void setBitmap(Bitmap bitmap) {
|
||||
ThreadUtil.assertMainThread();
|
||||
if (bmp == null || !bmp.sameAs(bitmap)) {
|
||||
bmp = bitmap;
|
||||
invalidateSelf();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.FontMetricsInt;
|
||||
@@ -25,6 +26,15 @@ public class EmojiSpan extends AnimatingImageSpan {
|
||||
getDrawable().setBounds(0, 0, size, size);
|
||||
}
|
||||
|
||||
public EmojiSpan(@NonNull Context context, @NonNull Drawable drawable, @NonNull Paint paint) {
|
||||
super(drawable, null);
|
||||
fontMetrics = paint.getFontMetricsInt();
|
||||
size = fontMetrics != null ? Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent)
|
||||
: context.getResources().getDimensionPixelSize(R.dimen.conversation_item_body_text_size);
|
||||
|
||||
getDrawable().setBounds(0, 0, size, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
|
||||
if (fm != null && this.fontMetrics != null) {
|
||||
@@ -48,6 +58,7 @@ public class EmojiSpan extends AnimatingImageSpan {
|
||||
int height = bottom - top;
|
||||
int centeringMargin = (height - size) / 2;
|
||||
int adjustedMargin = (int) (centeringMargin * SHIFT_FACTOR);
|
||||
int adjustedBottom = bottom - adjustedMargin;
|
||||
super.draw(canvas, text, start, end, x, top, y, bottom - adjustedMargin, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.text.Spanned;
|
||||
import android.text.TextDirectionHeuristic;
|
||||
import android.text.TextDirectionHeuristics;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.TransformationMethod;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ViewGroup;
|
||||
@@ -42,6 +43,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private boolean forceCustom;
|
||||
private CharSequence previousText;
|
||||
private BufferType previousBufferType;
|
||||
private TransformationMethod previousTransformationMethod;
|
||||
private float originalFontSize;
|
||||
private boolean useSystemEmoji;
|
||||
private boolean sizeChangeInProgress;
|
||||
@@ -124,10 +126,11 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
return;
|
||||
}
|
||||
|
||||
previousText = text;
|
||||
previousOverflowText = overflowText;
|
||||
previousBufferType = type;
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
previousText = text;
|
||||
previousOverflowText = overflowText;
|
||||
previousBufferType = type;
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
previousTransformationMethod = getTransformationMethod();
|
||||
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.NORMAL);
|
||||
@@ -158,12 +161,13 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
lastLineWidth = -1;
|
||||
} else {
|
||||
Layout layout = getLayout();
|
||||
int lines = layout.getLineCount();
|
||||
int start = layout.getLineStart(lines - 1);
|
||||
int count = text.length() - start;
|
||||
text = layout.getText();
|
||||
|
||||
if ((getLayoutDirection() == LAYOUT_DIRECTION_LTR && textDirection.isRtl(text, start, count)) ||
|
||||
(getLayoutDirection() == LAYOUT_DIRECTION_RTL && !textDirection.isRtl(text, start, count))) {
|
||||
int lines = layout.getLineCount();
|
||||
int start = layout.getLineStart(lines - 1);
|
||||
|
||||
if ((getLayoutDirection() == LAYOUT_DIRECTION_LTR && textDirection.isRtl(text, 0, text.length())) ||
|
||||
(getLayoutDirection() == LAYOUT_DIRECTION_RTL && !textDirection.isRtl(text, 0, text.length()))) {
|
||||
lastLineWidth = getMeasuredWidth();
|
||||
} else {
|
||||
lastLineWidth = (int) getPaint().measureText(text, start, text.length());
|
||||
@@ -260,7 +264,8 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
Util.equals(previousOverflowText, overflowText) &&
|
||||
Util.equals(previousBufferType, bufferType) &&
|
||||
useSystemEmoji == useSystemEmoji() &&
|
||||
!sizeChangeInProgress;
|
||||
!sizeChangeInProgress &&
|
||||
previousTransformationMethod == getTransformationMethod();
|
||||
}
|
||||
|
||||
private boolean useSystemEmoji() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -20,6 +21,8 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
private static final String TAG = Log.tag(MediaKeyboard.class);
|
||||
@@ -40,6 +43,10 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setFragmentManager(@NonNull FragmentManager fragmentManager) {
|
||||
this.fragmentManager = fragmentManager;
|
||||
}
|
||||
|
||||
public void setKeyboardListener(@Nullable MediaKeyboardListener listener) {
|
||||
this.keyboardListener = listener;
|
||||
}
|
||||
@@ -125,13 +132,32 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
private void initView() {
|
||||
if (!isInitialised) {
|
||||
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
|
||||
|
||||
if (fragmentManager == null) {
|
||||
FragmentActivity activity = resolveActivity(getContext());
|
||||
fragmentManager = activity.getSupportFragmentManager();
|
||||
}
|
||||
|
||||
keyboardPagerFragment = new KeyboardPagerFragment();
|
||||
fragmentManager.beginTransaction()
|
||||
.replace(R.id.media_keyboard_fragment_container, keyboardPagerFragment)
|
||||
.commitNowAllowingStateLoss();
|
||||
|
||||
keyboardState = State.NORMAL;
|
||||
latestKeyboardHeight = -1;
|
||||
isInitialised = true;
|
||||
fragmentManager = ((FragmentActivity) getContext()).getSupportFragmentManager();
|
||||
keyboardPagerFragment = (KeyboardPagerFragment) fragmentManager.findFragmentById(R.id.media_keyboard_fragment_container);
|
||||
}
|
||||
}
|
||||
|
||||
private static FragmentActivity resolveActivity(@Nullable Context context) {
|
||||
if (context instanceof FragmentActivity) {
|
||||
return (FragmentActivity) context;
|
||||
} else if (context instanceof ContextThemeWrapper) {
|
||||
return resolveActivity(((ContextThemeWrapper) context).getBaseContext());
|
||||
} else {
|
||||
throw new IllegalStateException("Could not locate FragmentActivity");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.appcompat.widget.AppCompatTextView
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
|
||||
open class SingleLineEmojiTextView @JvmOverloads constructor(
|
||||
open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
@@ -15,20 +15,16 @@ open class SingleLineEmojiTextView @JvmOverloads constructor(
|
||||
|
||||
private var bufferType: BufferType? = null
|
||||
|
||||
init {
|
||||
maxLines = 1
|
||||
}
|
||||
|
||||
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||
bufferType = type
|
||||
val candidates = if (isInEditMode) null else EmojiProvider.getCandidates(text)
|
||||
if (SignalStore.settings().isPreferSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(Optional.fromNullable(text).or(""), BufferType.NORMAL)
|
||||
} else {
|
||||
val newContent = if (width == 0) {
|
||||
val newContent = if (width == 0 || maxLines == -1) {
|
||||
text
|
||||
} else {
|
||||
TextUtils.ellipsize(text, paint, width.toFloat(), TextUtils.TruncateAt.END, false, null)
|
||||
TextUtils.ellipsize(text, paint, (width * maxLines).toFloat(), TextUtils.TruncateAt.END, false, null)
|
||||
}
|
||||
|
||||
val newCandidates = if (isInEditMode) null else EmojiProvider.getCandidates(newContent)
|
||||
@@ -37,6 +33,7 @@ open class SingleLineEmojiTextView @JvmOverloads constructor(
|
||||
} else {
|
||||
EmojiProvider.emojify(newCandidates, newContent, this)
|
||||
}
|
||||
bufferType = BufferType.SPANNABLE
|
||||
super.setText(newText, type)
|
||||
}
|
||||
}
|
||||
@@ -47,9 +44,4 @@ open class SingleLineEmojiTextView @JvmOverloads constructor(
|
||||
setText(text, bufferType ?: BufferType.NORMAL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMaxLines(maxLines: Int) {
|
||||
check(maxLines == 1) { "setMaxLines: $maxLines != 1" }
|
||||
super.setMaxLines(maxLines)
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,11 @@ import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
@@ -40,12 +42,12 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
|
||||
identityStore.setApproval(identityRecord.getRecipientId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.util.List;
|
||||
@@ -39,27 +41,16 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
}
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
resendListener.onResendMessage();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
return null;
|
||||
}, nothing -> resendListener.onResendMessage());
|
||||
}
|
||||
|
||||
public interface ResendListener {
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.util.visible
|
||||
class DSLSettingsAdapter : MappingAdapter() {
|
||||
init {
|
||||
registerFactory(ClickPreference::class.java, LayoutFactory(::ClickPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(LongClickPreference::class.java, LayoutFactory(::LongClickPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(TextPreference::class.java, LayoutFactory(::TextPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(RadioListPreference::class.java, LayoutFactory(::RadioListPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(MultiSelectListPreference::class.java, LayoutFactory(::MultiSelectListPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
@@ -82,6 +83,16 @@ class ClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ClickPref
|
||||
}
|
||||
}
|
||||
|
||||
class LongClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<LongClickPreference>(itemView) {
|
||||
override fun bind(model: LongClickPreference) {
|
||||
super.bind(model)
|
||||
itemView.setOnLongClickListener() {
|
||||
model.onLongClick()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<RadioListPreference>(itemView) {
|
||||
override fun bind(model: RadioListPreference) {
|
||||
super.bind(model)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -10,6 +11,7 @@ import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
@@ -18,7 +20,8 @@ import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimation
|
||||
abstract class DSLSettingsFragment(
|
||||
@StringRes private val titleId: Int = -1,
|
||||
@MenuRes private val menuId: Int = -1,
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment,
|
||||
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
||||
) : Fragment(layoutId) {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
@@ -46,6 +49,7 @@ abstract class DSLSettingsFragment(
|
||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
val adapter = DSLSettingsAdapter()
|
||||
|
||||
recyclerView.layoutManager = layoutManagerProducer(requireContext())
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(scrollAnimationHelper)
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
.setStartCategoryIndex(intent.getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0))
|
||||
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
|
||||
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +99,9 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
@JvmStatic
|
||||
fun notifications(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATIONS)
|
||||
|
||||
@JvmStatic
|
||||
fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHANGE_NUMBER)
|
||||
|
||||
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||
return Intent(context, AppSettingsActivity::class.java)
|
||||
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||
@@ -110,7 +114,8 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
BACKUPS(1),
|
||||
HELP(2),
|
||||
PROXY(3),
|
||||
NOTIFICATIONS(4);
|
||||
NOTIFICATIONS(4),
|
||||
CHANGE_NUMBER(5);
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int?): StartLocation {
|
||||
|
||||
@@ -2,9 +2,10 @@ package org.thoughtcrime.securesms.components.settings.app
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
@@ -27,7 +28,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
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))
|
||||
|
||||
val viewModel = ViewModelProviders.of(this)[AppSettingsViewModel::class.java]
|
||||
val viewModel = ViewModelProvider(this)[AppSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
@@ -162,6 +163,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
private val avatarView: AvatarImageView = itemView.findViewById(R.id.icon)
|
||||
private val aboutView: TextView = itemView.findViewById(R.id.about)
|
||||
private val badgeView: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||
|
||||
override fun bind(model: BioPreference) {
|
||||
super.bind(model)
|
||||
@@ -171,6 +173,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
titleView.text = model.recipient.profileName.toString()
|
||||
summaryView.text = PhoneNumberFormatter.prettyPrint(model.recipient.requireE164())
|
||||
avatarView.setRecipient(Recipient.self())
|
||||
badgeView.setBadgeFromRecipient(Recipient.self())
|
||||
|
||||
titleView.visibility = View.VISIBLE
|
||||
summaryView.visibility = View.VISIBLE
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.autofill.HintConstants
|
||||
import androidx.core.app.DialogCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -30,6 +30,8 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsConstants
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
|
||||
@@ -49,7 +51,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
viewModel = ViewModelProviders.of(this)[AccountSettingsViewModel::class.java]
|
||||
viewModel = ViewModelProvider(this)[AccountSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
@@ -61,6 +63,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
|
||||
sectionHeaderPref(R.string.preferences_app_protection__signal_pin)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(if (state.hasPin) R.string.preferences_app_protection__change_your_pin else R.string.preferences_app_protection__create_a_pin),
|
||||
onClick = {
|
||||
@@ -103,6 +106,15 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
|
||||
sectionHeaderPref(R.string.AccountSettingsFragment__account)
|
||||
|
||||
if (FeatureFlags.changeNumber() && Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
|
||||
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.appearance
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
@@ -23,7 +23,7 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
|
||||
private val languageValues by lazy { resources.getStringArray(R.array.language_values) }
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
viewModel = ViewModelProviders.of(this)[AppearanceSettingsViewModel::class.java]
|
||||
viewModel = ViewModelProvider(this)[AppearanceSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseAccountLockedFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
|
||||
class ChangeNumberAccountLockedFragment : BaseAccountLockedFragment(R.layout.fragment_change_number_account_locked) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
}
|
||||
|
||||
override fun getViewModel(): BaseRegistrationViewModel {
|
||||
return ChangeNumberUtil.getViewModel(this)
|
||||
}
|
||||
|
||||
override fun onNext() {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_number_confirm) {
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel = ChangeNumberUtil.getViewModel(this)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
val confirmMessage: TextView = view.findViewById(R.id.change_number_confirm_new_number_message)
|
||||
confirmMessage.text = getString(R.string.ChangeNumberConfirmFragment__you_are_about_to_change_your_phone_number_from_s_to_s, viewModel.oldNumberState.fullFormattedNumber, viewModel.number.fullFormattedNumber)
|
||||
|
||||
val newNumber: TextView = view.findViewById(R.id.change_number_confirm_new_number)
|
||||
newNumber.text = viewModel.number.fullFormattedNumber
|
||||
|
||||
val editNumber: View = view.findViewById(R.id.change_number_confirm_edit_number)
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseEnterCodeFragment
|
||||
|
||||
class ChangeNumberEnterCodeFragment : BaseEnterCodeFragment<ChangeNumberViewModel>(R.layout.fragment_change_number_enter_code) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.title = viewModel.number.fullFormattedNumber
|
||||
toolbar.setNavigationOnClickListener { navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.verify_header).setOnClickListener(null)
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateUp() {
|
||||
if (SignalStore.misc().isChangeNumberLocked) {
|
||||
startActivity(ChangeNumberLockActivity.createIntent(requireContext()))
|
||||
} else {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewModel(): ChangeNumberViewModel {
|
||||
return getViewModel(this)
|
||||
}
|
||||
|
||||
override fun handleSuccessfulVerify() {
|
||||
displaySuccess { changeNumberSuccess() }
|
||||
}
|
||||
|
||||
override fun navigateToCaptcha() {
|
||||
findNavController().navigate(R.id.action_changeNumberEnterCodeFragment_to_captchaFragment, getCaptchaArguments())
|
||||
}
|
||||
|
||||
override fun navigateToRegistrationLock(timeRemaining: Long) {
|
||||
findNavController().navigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
}
|
||||
|
||||
override fun navigateToKbsAccountLocked() {
|
||||
findNavController().navigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Spinner
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.LabeledEditText
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel.ContinueStatus
|
||||
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
|
||||
|
||||
private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country"
|
||||
private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country"
|
||||
|
||||
class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_change_number_enter_phone_number) {
|
||||
|
||||
private lateinit var scrollView: ScrollView
|
||||
|
||||
private lateinit var oldNumberCountrySpinner: Spinner
|
||||
private lateinit var oldNumberCountryCode: LabeledEditText
|
||||
private lateinit var oldNumber: LabeledEditText
|
||||
|
||||
private lateinit var newNumberCountrySpinner: Spinner
|
||||
private lateinit var newNumberCountryCode: LabeledEditText
|
||||
private lateinit var newNumber: LabeledEditText
|
||||
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel = getViewModel(this)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.change_number_enter_phone_number_continue).setOnClickListener {
|
||||
onContinue()
|
||||
}
|
||||
|
||||
scrollView = view.findViewById(R.id.change_number_enter_phone_number_scroll)
|
||||
|
||||
oldNumberCountrySpinner = view.findViewById(R.id.change_number_enter_phone_number_old_number_spinner)
|
||||
oldNumberCountryCode = view.findViewById(R.id.change_number_enter_phone_number_old_number_country_code)
|
||||
oldNumber = view.findViewById(R.id.change_number_enter_phone_number_old_number_number)
|
||||
|
||||
val oldController = RegistrationNumberInputController(
|
||||
requireContext(),
|
||||
oldNumberCountryCode,
|
||||
oldNumber,
|
||||
oldNumberCountrySpinner,
|
||||
false,
|
||||
object : RegistrationNumberInputController.Callbacks {
|
||||
override fun onNumberFocused() {
|
||||
scrollView.postDelayed({ scrollView.smoothScrollTo(0, oldNumber.bottom) }, 250)
|
||||
}
|
||||
|
||||
override fun onNumberInputNext(view: View) {
|
||||
newNumberCountryCode.requestFocus()
|
||||
}
|
||||
|
||||
override fun onNumberInputDone(view: View) = Unit
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
viewModel.setOldNationalNumber(number)
|
||||
}
|
||||
|
||||
override fun setCountry(countryCode: Int) {
|
||||
viewModel.setOldCountry(countryCode)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
newNumberCountrySpinner = view.findViewById(R.id.change_number_enter_phone_number_new_number_spinner)
|
||||
newNumberCountryCode = view.findViewById(R.id.change_number_enter_phone_number_new_number_country_code)
|
||||
newNumber = view.findViewById(R.id.change_number_enter_phone_number_new_number_number)
|
||||
|
||||
val newController = RegistrationNumberInputController(
|
||||
requireContext(),
|
||||
newNumberCountryCode,
|
||||
newNumber,
|
||||
newNumberCountrySpinner,
|
||||
true,
|
||||
object : RegistrationNumberInputController.Callbacks {
|
||||
override fun onNumberFocused() {
|
||||
scrollView.postDelayed({ scrollView.smoothScrollTo(0, newNumber.bottom) }, 250)
|
||||
}
|
||||
|
||||
override fun onNumberInputNext(view: View) = Unit
|
||||
|
||||
override fun onNumberInputDone(view: View) {
|
||||
onContinue()
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
viewModel.setNewNationalNumber(number)
|
||||
}
|
||||
|
||||
override fun setCountry(countryCode: Int) {
|
||||
viewModel.setNewCountry(countryCode)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(OLD_NUMBER_COUNTRY_SELECT, this) { _, bundle ->
|
||||
viewModel.setOldCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _, bundle ->
|
||||
viewModel.setNewCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
viewModel.getLiveOldNumber().observe(viewLifecycleOwner, oldController::updateNumber)
|
||||
viewModel.getLiveNewNumber().observe(viewLifecycleOwner, newController::updateNumber)
|
||||
}
|
||||
|
||||
private fun onContinue() {
|
||||
if (TextUtils.isEmpty(oldNumberCountryCode.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_number_country_code), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(oldNumber.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_phone_number), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(newNumberCountryCode.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_number_country_code), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(newNumber.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_phone_number), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
when (viewModel.canContinue()) {
|
||||
ContinueStatus.CAN_CONTINUE -> findNavController().navigate(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)
|
||||
)
|
||||
}
|
||||
ContinueStatus.OLD_NUMBER_DOESNT_MATCH -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.ChangeNumberEnterPhoneNumberFragment__the_phone_number_you_entered_doesnt_match_your_accounts)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class ChangeNumberFragment : LoggingFragment(R.layout.fragment_change_phone_number) {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.change_phone_number_continue).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
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.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.Objects
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
||||
|
||||
/**
|
||||
* A captive activity that can determine if an interrupted/erred change number request
|
||||
* caused a disparity between the server and our locally stored number.
|
||||
*/
|
||||
class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
|
||||
private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
private lateinit var changeNumberRepository: ChangeNumberRepository
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
dynamicTheme.onCreate(this)
|
||||
disposables.bindTo(lifecycle)
|
||||
|
||||
setContentView(R.layout.activity_change_number_lock)
|
||||
|
||||
changeNumberRepository = ChangeNumberRepository(applicationContext)
|
||||
checkWhoAmI()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
|
||||
override fun onBackPressed() = Unit
|
||||
|
||||
private fun checkWhoAmI() {
|
||||
disposables.add(
|
||||
changeNumberRepository.whoAmI()
|
||||
.flatMap { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, TextSecurePreferences.getLocalNumber(this))) {
|
||||
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${TextSecurePreferences.getLocalNumber(this)}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number)
|
||||
.map { true }
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChangeStatusConfirmed() {
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)
|
||||
.setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(TextSecurePreferences.getLocalNumber(this))))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
finish()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onFailedToGetChangeNumberStatus(error: Throwable) {
|
||||
Log.w(TAG, "Unable to determine status of change number", error)
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.ChangeNumberLockActivity__change_status_unconfirmed)
|
||||
.setMessage(getString(R.string.ChangeNumberLockActivity__we_could_not_determine_the_status_of_your_change_number_request, error.javaClass.simpleName))
|
||||
.setPositiveButton(R.string.ChangeNumberLockActivity__retry) { _, _ -> checkWhoAmI() }
|
||||
.setNegativeButton(R.string.ChangeNumberLockActivity__leave) { _, _ -> finish() }
|
||||
.setNeutralButton(R.string.ChangeNumberLockActivity__submit_debug_log) { _, _ ->
|
||||
startActivity(Intent(this, SubmitDebugLogActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, ChangeNumberLockActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity
|
||||
|
||||
class ChangeNumberPinDiffersFragment : LoggingFragment(R.layout.fragment_change_number_pin_differs) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
view.findViewById<View>(R.id.change_number_pin_differs_keep_old_pin).setOnClickListener {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
|
||||
val changePin = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == CreateKbsPinActivity.RESULT_OK) {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.change_number_pin_differs_update_pin).setOnClickListener {
|
||||
changePin.launch(CreateKbsPinActivity.getIntentForPinChangeFromSettings(requireContext()))
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.ChangeNumberPinDiffersFragment__keep_old_pin_question)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> changeNumberSuccess() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.PinHashing
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
|
||||
class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layout.fragment_change_number_registration_lock) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { navigateUp() }
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun getViewModel(): BaseRegistrationViewModel {
|
||||
return ChangeNumberUtil.getViewModel(this)
|
||||
}
|
||||
|
||||
override fun navigateToAccountLocked() {
|
||||
findNavController().navigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked())
|
||||
}
|
||||
|
||||
override fun handleSuccessfulPinEntry(pin: String) {
|
||||
val pinsDiffer: Boolean = SignalStore.kbsValues().localPinHash?.let { !PinHashing.verifyLocalPinHash(it, pin) } ?: false
|
||||
|
||||
cancelSpinning(pinButton)
|
||||
|
||||
if (pinsDiffer) {
|
||||
findNavController().navigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
|
||||
} else {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendEmailToSupport() {
|
||||
val subject = R.string.ChangeNumberRegistrationLockFragment__signal_change_number_need_help_with_pin_for_android_v2_pin
|
||||
|
||||
val body: String = SupportEmailUtil.generateSupportEmailBody(
|
||||
requireContext(),
|
||||
subject,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
CommunicationActions.openEmail(
|
||||
requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(subject),
|
||||
body
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateUp() {
|
||||
if (SignalStore.misc().isChangeNumberLocked) {
|
||||
startActivity(ChangeNumberLockActivity.createIntent(requireContext()))
|
||||
} else {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||
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.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
|
||||
|
||||
class ChangeNumberRepository(private val context: Context) {
|
||||
|
||||
private val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
|
||||
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
return Single.fromCallable { accountManager.changeNumber(code, newE164, null) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun changeNumber(
|
||||
code: String,
|
||||
newE164: String,
|
||||
pin: String,
|
||||
tokenData: TokenData
|
||||
): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
|
||||
return Single.fromCallable {
|
||||
try {
|
||||
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
|
||||
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock()
|
||||
|
||||
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(code, newE164, registrationLock)
|
||||
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: KeyBackupSystemNoDataException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
fun whoAmI(): Single<WhoAmIResponse> {
|
||||
return Single.fromCallable { ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun changeLocalNumber(e164: String): Single<Unit> {
|
||||
DatabaseFactory.getRecipientDatabase(context).updateSelfPhone(e164)
|
||||
|
||||
TextSecurePreferences.setLocalNumber(context, e164)
|
||||
|
||||
ApplicationDependencies.closeConnections()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
|
||||
return rotateCertificates()
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
private fun rotateCertificates(): Single<Unit> {
|
||||
val certificateTypes = SignalStore.phoneNumberPrivacy().allCertificateTypes
|
||||
|
||||
Log.i(TAG, "Rotating these certificates $certificateTypes")
|
||||
|
||||
return Single.fromCallable {
|
||||
for (certificateType in certificateTypes) {
|
||||
val certificate: ByteArray? = when (certificateType) {
|
||||
CertificateType.UUID_AND_E164 -> accountManager.getSenderCertificate()
|
||||
CertificateType.UUID_ONLY -> accountManager.getSenderCertificateForPhoneNumberPrivacy()
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully got $certificateType certificate")
|
||||
|
||||
SignalStore.certificateValues().setUnidentifiedAccessCertificate(certificateType, certificate)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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.registration.fragments.CaptchaFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
|
||||
/**
|
||||
* Helpers for various aspects of the change number flow.
|
||||
*/
|
||||
object ChangeNumberUtil {
|
||||
@JvmStatic
|
||||
fun getViewModel(fragment: Fragment): ChangeNumberViewModel {
|
||||
val navController = NavHostFragment.findNavController(fragment)
|
||||
return ViewModelProvider(
|
||||
navController.getViewModelStoreOwner(R.id.app_settings_change_number),
|
||||
ChangeNumberViewModel.Factory(navController.getBackStackEntry(R.id.app_settings_change_number))
|
||||
).get(ChangeNumberViewModel::class.java)
|
||||
}
|
||||
|
||||
fun getCaptchaArguments(): Bundle {
|
||||
return Bundle().apply {
|
||||
putSerializable(
|
||||
CaptchaFragment.EXTRA_VIEW_MODEL_PROVIDER,
|
||||
object : CaptchaFragment.CaptchaViewModelProvider {
|
||||
override fun get(fragment: CaptchaFragment): BaseRegistrationViewModel = getViewModel(fragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java)
|
||||
|
||||
class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phone_number_verify) {
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
private var requestingCaptcha: Boolean = false
|
||||
|
||||
private val lifecycleDisposable: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleDisposable.bindTo(lifecycle)
|
||||
viewModel = getViewModel(this)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberVerifyFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
val status: TextView = view.findViewById(R.id.change_phone_number_verify_status)
|
||||
status.text = getString(R.string.ChangeNumberVerifyFragment__verifying_s, viewModel.number.fullFormattedNumber)
|
||||
|
||||
if (!requestingCaptcha || viewModel.hasCaptchaToken()) {
|
||||
requestCode()
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.ChangeNumberVerifyFragment__captcha_required, Toast.LENGTH_SHORT).show()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestCode() {
|
||||
lifecycleDisposable.add(
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
findNavController().navigate(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)
|
||||
} 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())
|
||||
requestingCaptcha = true
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request sms code", processor.error)
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.app.Application
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.AbstractSavedStateViewModelFactory
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs
|
||||
import org.thoughtcrime.securesms.registration.VerifyCodeWithRegistrationLockResponseProcessor
|
||||
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.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.util.Objects
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberViewModel::class.java)
|
||||
|
||||
class ChangeNumberViewModel(
|
||||
private val localNumber: String,
|
||||
private val changeNumberRepository: ChangeNumberRepository,
|
||||
savedState: SavedStateHandle,
|
||||
password: String,
|
||||
verifyAccountRepository: VerifyAccountRepository,
|
||||
kbsRepository: KbsRepository,
|
||||
) : BaseRegistrationViewModel(savedState, verifyAccountRepository, kbsRepository, password) {
|
||||
|
||||
var oldNumberState: NumberViewState = NumberViewState.Builder().build()
|
||||
private set
|
||||
|
||||
private val liveOldNumberState = DefaultValueLiveData(oldNumberState)
|
||||
private val liveNewNumberState = DefaultValueLiveData(number)
|
||||
|
||||
init {
|
||||
try {
|
||||
val countryCode: Int = PhoneNumberUtil.getInstance()
|
||||
.parse(localNumber, null)
|
||||
.countryCode
|
||||
|
||||
setOldCountry(countryCode)
|
||||
setNewCountry(countryCode)
|
||||
} catch (e: NumberParseException) {
|
||||
Log.i(TAG, "Unable to parse number for default country code")
|
||||
}
|
||||
}
|
||||
|
||||
fun getLiveOldNumber(): LiveData<NumberViewState> {
|
||||
return liveOldNumberState
|
||||
}
|
||||
|
||||
fun getLiveNewNumber(): LiveData<NumberViewState> {
|
||||
return liveNewNumberState
|
||||
}
|
||||
|
||||
fun setOldNationalNumber(number: String) {
|
||||
oldNumberState = oldNumberState.toBuilder()
|
||||
.nationalNumber(number)
|
||||
.build()
|
||||
|
||||
liveOldNumberState.value = oldNumberState
|
||||
}
|
||||
|
||||
fun setOldCountry(countryCode: Int, country: String? = null) {
|
||||
oldNumberState = oldNumberState.toBuilder()
|
||||
.selectedCountryDisplayName(country)
|
||||
.countryCode(countryCode)
|
||||
.build()
|
||||
|
||||
liveOldNumberState.value = oldNumberState
|
||||
}
|
||||
|
||||
fun setNewNationalNumber(number: String) {
|
||||
setNationalNumber(number)
|
||||
|
||||
liveNewNumberState.value = this.number
|
||||
}
|
||||
|
||||
fun setNewCountry(countryCode: Int, country: String? = null) {
|
||||
onCountrySelected(country, countryCode)
|
||||
|
||||
liveNewNumberState.value = this.number
|
||||
}
|
||||
|
||||
fun canContinue(): ContinueStatus {
|
||||
return if (oldNumberState.e164Number == localNumber) {
|
||||
if (number.isValid) {
|
||||
ContinueStatus.CAN_CONTINUE
|
||||
} else {
|
||||
ContinueStatus.INVALID_NUMBER
|
||||
}
|
||||
} else {
|
||||
ContinueStatus.OLD_NUMBER_DOESNT_MATCH
|
||||
}
|
||||
}
|
||||
|
||||
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> {
|
||||
return super.verifyCodeWithoutRegistrationLock(code)
|
||||
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
|
||||
.flatMap(this::attemptToUnlockChangeNumber)
|
||||
}
|
||||
|
||||
override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
|
||||
return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
|
||||
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
|
||||
.flatMap(this::attemptToUnlockChangeNumber)
|
||||
}
|
||||
|
||||
private fun <T : VerifyProcessor> attemptToUnlockChangeNumber(processor: T): Single<T> {
|
||||
return if (processor.hasResult() || processor.isServerSentError()) {
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
Single.just(processor)
|
||||
} else {
|
||||
changeNumberRepository.whoAmI()
|
||||
.map { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, localNumber)) {
|
||||
Log.i(TAG, "Local and remote numbers match, we can unlock.")
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
}
|
||||
processor
|
||||
}
|
||||
.onErrorReturn { processor }
|
||||
}
|
||||
}
|
||||
|
||||
override fun verifyAccountWithoutRegistrationLock(): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
return changeNumberRepository.changeNumber(textCodeEntered, number.e164Number)
|
||||
}
|
||||
|
||||
override fun verifyAccountWithRegistrationLock(pin: String, kbsTokenData: TokenData): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
|
||||
return changeNumberRepository.changeNumber(textCodeEntered, number.e164Number, pin, kbsTokenData)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onVerifySuccess(processor: VerifyAccountResponseProcessor): Single<VerifyAccountResponseProcessor> {
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number)
|
||||
.map { processor }
|
||||
.onErrorReturn { t ->
|
||||
Log.w(TAG, "Error attempting to change local number", t)
|
||||
VerifyAccountResponseWithoutKbs(ServiceResponse.forUnknownError(t))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVerifySuccessWithRegistrationLock(processor: VerifyCodeWithRegistrationLockResponseProcessor, pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number)
|
||||
.map { processor }
|
||||
.onErrorReturn { t ->
|
||||
Log.w(TAG, "Error attempting to change local number", t)
|
||||
VerifyCodeWithRegistrationLockResponseProcessor(ServiceResponse.forUnknownError(t), processor.token)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(owner: SavedStateRegistryOwner) : AbstractSavedStateViewModelFactory(owner, null) {
|
||||
|
||||
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
|
||||
val context: Application = ApplicationDependencies.getApplication()
|
||||
val localNumber: String = TextSecurePreferences.getLocalNumber(context)
|
||||
val password: String = TextSecurePreferences.getPushServerPassword(context)
|
||||
|
||||
val viewModel = ChangeNumberViewModel(
|
||||
localNumber = localNumber,
|
||||
changeNumberRepository = ChangeNumberRepository(context),
|
||||
savedState = handle,
|
||||
password = password,
|
||||
verifyAccountRepository = VerifyAccountRepository(context),
|
||||
kbsRepository = KbsRepository()
|
||||
)
|
||||
|
||||
return requireNotNull(modelClass.cast(viewModel))
|
||||
}
|
||||
}
|
||||
|
||||
enum class ContinueStatus {
|
||||
CAN_CONTINUE,
|
||||
INVALID_NUMBER,
|
||||
OLD_NUMBER_DOESNT_MATCH
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
@@ -16,7 +16,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
val repository = ChatsSettingsRepository()
|
||||
val factory = ChatsSettingsViewModel.Factory(repository)
|
||||
viewModel = ViewModelProviders.of(this, factory)[ChatsSettingsViewModel::class.java]
|
||||
viewModel = ViewModelProvider(this, factory)[ChatsSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
@@ -28,7 +28,7 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
viewModel = ViewModelProviders.of(this)[SmsSettingsViewModel::class.java]
|
||||
viewModel = ViewModelProvider(this)[SmsSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
@@ -41,6 +41,7 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||
|
||||
private fun getConfiguration(state: SmsSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
@Suppress("DEPRECATION")
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app),
|
||||
summary = DSLSettingsText.from(if (state.useAsDefaultSmsApp) R.string.arrays__enabled else R.string.arrays__disabled),
|
||||
@@ -84,6 +85,7 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||
|
||||
// Linter isn't smart enough to figure out the else only happens if API >= 24
|
||||
@SuppressLint("InlinedApi")
|
||||
@Suppress("DEPRECATION")
|
||||
private fun startDefaultAppSelectionIntent() {
|
||||
val intent: Intent = when {
|
||||
Build.VERSION.SDK_INT < 23 -> Intent(Settings.ACTION_WIRELESS_SETTINGS)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.data
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -31,7 +31,7 @@ class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences_
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val repository = DataAndStorageSettingsRepository()
|
||||
val factory = DataAndStorageSettingsViewModel.Factory(preferences, repository)
|
||||
viewModel = ViewModelProviders.of(this, factory)[DataAndStorageSettingsViewModel::class.java]
|
||||
viewModel = ViewModelProvider(this, factory)[DataAndStorageSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
@@ -34,7 +34,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
val repository = InternalSettingsRepository(requireContext())
|
||||
val factory = InternalSettingsViewModel.Factory(repository)
|
||||
viewModel = ViewModelProviders.of(this, factory)[InternalSettingsViewModel::class.java]
|
||||
viewModel = ViewModelProvider(this, factory)[InternalSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
@@ -107,6 +107,15 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_storage_service)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_disable_storage_service),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_disable_storage_service_description),
|
||||
isChecked = state.disableStorageService,
|
||||
onClick = {
|
||||
viewModel.setDisableStorageService(!state.disableStorageService)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_force_storage_service_sync),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_force_storage_service_sync_description),
|
||||
@@ -221,7 +230,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
summary = DSLSettingsText.from(emojiSummary),
|
||||
isChecked = state.useBuiltInEmojiSet,
|
||||
onClick = {
|
||||
viewModel.setDisableAutoMigrationNotification(!state.useBuiltInEmojiSet)
|
||||
viewModel.setUseBuiltInEmoji(!state.useBuiltInEmojiSet)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -314,12 +323,12 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
.setPositiveButton(
|
||||
"Copy"
|
||||
) { _: DialogInterface?, _: Int ->
|
||||
val context: Context = ApplicationDependencies.getApplication()
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
SimpleTask.run<Any?>(
|
||||
SignalExecutors.UNBOUNDED,
|
||||
{
|
||||
val context: Context = ApplicationDependencies.getApplication()
|
||||
val clipboard =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val tsv = DataExportUtil.createTsv()
|
||||
val clip = ClipData.newPlainText(context.getString(R.string.app_name), tsv)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
|
||||
@@ -17,4 +17,5 @@ data class InternalSettingsState(
|
||||
val emojiVersion: EmojiFiles.Version?,
|
||||
val removeSenderKeyMinimium: Boolean,
|
||||
val delayResends: Boolean,
|
||||
val disableStorageService: Boolean,
|
||||
)
|
||||
|
||||
@@ -30,6 +30,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setDisableStorageService(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.DISABLE_STORAGE_SERVICE, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setGv2DoNotCreateGv2Groups(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.GV2_DO_NOT_CREATE_GV2, enabled)
|
||||
refresh()
|
||||
@@ -103,7 +108,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
useBuiltInEmojiSet = SignalStore.internalValues().forceBuiltInEmoji(),
|
||||
emojiVersion = null,
|
||||
removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum(),
|
||||
delayResends = SignalStore.internalValues().delayResends()
|
||||
delayResends = SignalStore.internalValues().delayResends(),
|
||||
disableStorageService = SignalStore.internalValues().storageServiceDisabled()
|
||||
)
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -12,7 +12,7 @@ import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
@@ -71,7 +71,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val factory = NotificationsSettingsViewModel.Factory(sharedPreferences)
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory)[NotificationsSettingsViewModel::class.java]
|
||||
viewModel = ViewModelProvider(this, factory)[NotificationsSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
@@ -241,6 +241,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun launchMessageSoundSelectionIntent() {
|
||||
val current = SignalStore.settings().messageNotificationSound
|
||||
|
||||
@@ -268,6 +269,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun launchCallRingtoneSelectionIntent() {
|
||||
val current = SignalStore.settings().callRingtone
|
||||
|
||||
|
||||