Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f02e2d23d0 | ||
|
|
ef1c25c3d3 | ||
|
|
152cc27394 | ||
|
|
c582aca465 | ||
|
|
80e85fb49a | ||
|
|
d660e22e61 | ||
|
|
51856c4f06 | ||
|
|
fd37da42f9 | ||
|
|
11df2bc51f | ||
|
|
6770d21cf7 | ||
|
|
f490d1f6d2 | ||
|
|
f890ae8ddc | ||
|
|
5d5d61d8ed | ||
|
|
75589f1b2d | ||
|
|
6225c676e2 | ||
|
|
9b18668f49 | ||
|
|
2f80e7f1ff | ||
|
|
790413680d | ||
|
|
47e9a4ec29 | ||
|
|
defd5e8047 | ||
|
|
8c6a88374b | ||
|
|
7343613bea | ||
|
|
155dda1fa4 | ||
|
|
3c74306c8d | ||
|
|
13ecd9eee6 | ||
|
|
c48f3b4582 | ||
|
|
30c007194d | ||
|
|
ef5b68eb35 | ||
|
|
c47dcd5720 | ||
|
|
ed3c5ab479 | ||
|
|
a697b6c3d4 | ||
|
|
3965df78c9 | ||
|
|
64ebf20c1b | ||
|
|
797bed6701 | ||
|
|
e84e021127 | ||
|
|
0b9515b58b | ||
|
|
81ec9e96c7 | ||
|
|
ee09793ef2 | ||
|
|
61a130e645 | ||
|
|
19d342749a | ||
|
|
94adcf04f5 | ||
|
|
53e1da0f43 | ||
|
|
b41989de03 | ||
|
|
6c7848b750 | ||
|
|
07bd9ad840 | ||
|
|
14236d3062 | ||
|
|
6c4df30252 | ||
|
|
45218470af | ||
|
|
417ee1e047 | ||
|
|
08a3bc457e | ||
|
|
0cc2cba883 | ||
|
|
24d461c8b2 | ||
|
|
4d472fccd2 | ||
|
|
45d010bdb6 | ||
|
|
70db617229 | ||
|
|
d8256013a3 | ||
|
|
6d2c22addc | ||
|
|
9640f3f215 | ||
|
|
80c911e118 | ||
|
|
f2d5ea0391 | ||
|
|
a94d77d81e | ||
|
|
2d2de1a652 | ||
|
|
01f8823fb2 | ||
|
|
260575d139 | ||
|
|
1fb3290038 | ||
|
|
37596320e8 | ||
|
|
7a959c2c3e | ||
|
|
877c03e6a1 | ||
|
|
d3431d227b | ||
|
|
fbf307bf01 | ||
|
|
d672857e82 | ||
|
|
dd934e0095 | ||
|
|
8c9df8d3be | ||
|
|
b3aec58e69 | ||
|
|
b4111cffef | ||
|
|
ecc8d1738e | ||
|
|
d1982cbc0a | ||
|
|
03b65ce6dc | ||
|
|
56ea11cdff | ||
|
|
7a02404f7b | ||
|
|
b9a960a7c8 | ||
|
|
02c87a4d7b | ||
|
|
0dd9cd82f8 | ||
|
|
ec486d66f7 | ||
|
|
69cd7eb449 | ||
|
|
1427de7c65 | ||
|
|
8ad66e1e5e | ||
|
|
a2e31e97db | ||
|
|
1f3e131690 | ||
|
|
276b757e2d | ||
|
|
093df70602 | ||
|
|
fe9ab66f31 | ||
|
|
138f9476ac | ||
|
|
cb9ab61b6b | ||
|
|
bcbd365326 | ||
|
|
afdf4e365f | ||
|
|
553b7522aa | ||
|
|
13f38dd594 | ||
|
|
31e1c6f7aa | ||
|
|
02d060ca0a | ||
|
|
5e2a3ac644 | ||
|
|
2fc461b85f | ||
|
|
29a0b86411 | ||
|
|
efc3e7b25d | ||
|
|
6c2adfeec2 | ||
|
|
3124d6d43e | ||
|
|
e5a6b7d47d | ||
|
|
add65cf592 | ||
|
|
129effd0ec | ||
|
|
2aad00df85 | ||
|
|
85e0e74bc6 | ||
|
|
7fa200401c | ||
|
|
1a452efbb9 | ||
|
|
eb4bdf1db2 | ||
|
|
d58f68cb44 | ||
|
|
2f30d29351 | ||
|
|
dc6dc192dc | ||
|
|
751afadebd | ||
|
|
ac71c02dfa | ||
|
|
4567da193e | ||
|
|
bd2a1d5574 | ||
|
|
ab44d608d2 | ||
|
|
cdc7f1565e | ||
|
|
1493581a4d | ||
|
|
4461d6cf7f | ||
|
|
38e64b1f75 | ||
|
|
eb1daf4a20 | ||
|
|
e0c38f7c72 | ||
|
|
5638ff4a3a | ||
|
|
f4ae39dd44 | ||
|
|
d235125138 | ||
|
|
78d7759197 | ||
|
|
eddaad3b05 | ||
|
|
f2c80c800c | ||
|
|
a0e787e424 | ||
|
|
c9d1fb8533 | ||
|
|
006eebb09e | ||
|
|
4aec824bfd | ||
|
|
56f7564ce4 | ||
|
|
f89daefd43 | ||
|
|
8572a2d262 | ||
|
|
d6e41be4b4 | ||
|
|
5e715ffcce | ||
|
|
5100341e60 | ||
|
|
5ca4db6ea5 | ||
|
|
e02e07ae7a | ||
|
|
f3caedc045 | ||
|
|
59c49254e7 | ||
|
|
ad81b310e3 | ||
|
|
bf124b87fa | ||
|
|
a4868602b5 | ||
|
|
763aeabddd | ||
|
|
71fc5af320 | ||
|
|
d28f0e3544 | ||
|
|
b4d0dde129 | ||
|
|
281630e751 | ||
|
|
32d79ead15 | ||
|
|
5a91c7e84a | ||
|
|
e2e1200c89 | ||
|
|
ed1be76606 | ||
|
|
a64de91781 | ||
|
|
e802c8b8cc | ||
|
|
86f2cf0ac4 | ||
|
|
a844a6b6c1 | ||
|
|
c31146e902 |
190
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="RIGHT_MARGIN" value="240" />
|
||||||
|
<option name="FORMATTER_TAGS_ENABLED" value="true" />
|
||||||
|
<option name="SOFT_MARGINS" value="160" />
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
||||||
|
<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" />
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="JAVA">
|
||||||
|
<option name="BRACE_STYLE" value="5" />
|
||||||
|
<option name="CLASS_BRACE_STYLE" value="5" />
|
||||||
|
<option name="METHOD_BRACE_STYLE" value="5" />
|
||||||
|
<option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_ASSIGNMENT" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_THROWS_LIST" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_EXTENDS_LIST" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
|
||||||
|
<option name="ALIGN_GROUP_FIELD_DECLARATIONS" value="true" />
|
||||||
|
<option name="ALIGN_CONSECUTIVE_VARIABLE_DECLARATIONS" value="true" />
|
||||||
|
<option name="ALIGN_CONSECUTIVE_ASSIGNMENTS" value="true" />
|
||||||
|
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
|
||||||
|
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
|
||||||
|
<option name="WRAP_FIRST_METHOD_IN_CALL_CHAIN" value="true" />
|
||||||
|
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_MULTIPLE_EXPRESSIONS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="METHOD_ANNOTATION_WRAP" value="0" />
|
||||||
|
<option name="CLASS_ANNOTATION_WRAP" value="0" />
|
||||||
|
<option name="FIELD_ANNOTATION_WRAP" value="0" />
|
||||||
|
<option name="ENUM_CONSTANTS_WRAP" value="5" />
|
||||||
|
<option name="WRAP_ON_TYPING" value="0" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<groups>
|
||||||
|
<group>
|
||||||
|
<type>GETTERS_AND_SETTERS</type>
|
||||||
|
<order>KEEP</order>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<type>OVERRIDDEN_METHODS</type>
|
||||||
|
<order>KEEP</order>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<type>DEPENDENT_METHODS</type>
|
||||||
|
<order>BREADTH_FIRST</order>
|
||||||
|
</group>
|
||||||
|
</groups>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
@@ -25,12 +25,6 @@ repositories {
|
|||||||
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
|
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maven {
|
|
||||||
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
|
|
||||||
content {
|
|
||||||
includeGroupByRegex "org\\.signal.*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maven { // textdrawable
|
maven { // textdrawable
|
||||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
url 'https://dl.bintray.com/amulyakhare/maven'
|
||||||
content {
|
content {
|
||||||
@@ -61,8 +55,8 @@ protobuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 826
|
def canonicalVersionCode = 851
|
||||||
def canonicalVersionName = "5.8.10"
|
def canonicalVersionName = "5.12.3"
|
||||||
|
|
||||||
def postFixSize = 100
|
def postFixSize = 100
|
||||||
def abiPostFix = ['universal' : 0,
|
def abiPostFix = ['universal' : 0,
|
||||||
@@ -80,6 +74,10 @@ android {
|
|||||||
flavorDimensions 'distribution', 'environment'
|
flavorDimensions 'distribution', 'environment'
|
||||||
useLibrary 'org.apache.http.legacy'
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs = ["-Xallow-result-return-type"]
|
||||||
|
}
|
||||||
|
|
||||||
dexOptions {
|
dexOptions {
|
||||||
javaMaxHeapSize "4g"
|
javaMaxHeapSize "4g"
|
||||||
}
|
}
|
||||||
@@ -131,6 +129,8 @@ android {
|
|||||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
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}"
|
||||||
|
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||||
|
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
@@ -280,6 +280,7 @@ android {
|
|||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||||
|
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +330,7 @@ dependencies {
|
|||||||
force = true
|
force = true
|
||||||
}
|
}
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.preference:preference:1.0.0'
|
implementation 'androidx.preference:preference:1.0.0'
|
||||||
@@ -374,7 +375,7 @@ dependencies {
|
|||||||
implementation project(':device-transfer')
|
implementation project(':device-transfer')
|
||||||
|
|
||||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||||
implementation 'org.whispersystems:signal-client-android:0.1.7'
|
implementation 'org.whispersystems:signal-client-android:0.5.1'
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||||
|
|
||||||
implementation('com.mobilecoin:android-sdk:1.0.0') {
|
implementation('com.mobilecoin:android-sdk:1.0.0') {
|
||||||
@@ -383,7 +384,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'org.signal:argon2:13.1@aar'
|
implementation 'org.signal:argon2:13.1@aar'
|
||||||
|
|
||||||
implementation 'org.signal:ringrtc-android:2.9.4'
|
implementation 'org.signal:ringrtc-android:2.9.6'
|
||||||
|
|
||||||
implementation "me.leolin:ShortcutBadger:1.1.22"
|
implementation "me.leolin:ShortcutBadger:1.1.22"
|
||||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||||
@@ -425,7 +426,10 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
|
||||||
|
implementation "net.zetetic:android-database-sqlcipher:4.4.3"
|
||||||
|
implementation "androidx.sqlite:sqlite:2.1.0"
|
||||||
|
|
||||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||||
exclude group: 'com.fasterxml.jackson.core'
|
exclude group: 'com.fasterxml.jackson.core'
|
||||||
exclude group: 'org.freemarker'
|
exclude group: 'org.freemarker'
|
||||||
@@ -454,6 +458,7 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyVerification {
|
dependencyVerification {
|
||||||
@@ -522,6 +527,10 @@ task signProductionWebsiteRelease {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getLastCommitTimestamp() {
|
def getLastCommitTimestamp() {
|
||||||
|
if (!(new File('.git').exists())) {
|
||||||
|
return System.currentTimeMillis().toString()
|
||||||
|
}
|
||||||
|
|
||||||
new ByteArrayOutputStream().withStream { os ->
|
new ByteArrayOutputStream().withStream { os ->
|
||||||
def result = exec {
|
def result = exec {
|
||||||
executable = 'git'
|
executable = 'git'
|
||||||
@@ -534,6 +543,10 @@ def getLastCommitTimestamp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getGitHash() {
|
def getGitHash() {
|
||||||
|
if (!(new File('.git').exists())) {
|
||||||
|
return "abcd1234"
|
||||||
|
}
|
||||||
|
|
||||||
def stdout = new ByteArrayOutputStream()
|
def stdout = new ByteArrayOutputStream()
|
||||||
exec {
|
exec {
|
||||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import net.sqlcipher.database.SQLiteStatement;
|
|||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
import org.thoughtcrime.securesms.util.Hex;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -237,7 +238,12 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
|||||||
case Cursor.FIELD_TYPE_FLOAT:
|
case Cursor.FIELD_TYPE_FLOAT:
|
||||||
return cursor.getDouble(column);
|
return cursor.getDouble(column);
|
||||||
case Cursor.FIELD_TYPE_BLOB:
|
case Cursor.FIELD_TYPE_BLOB:
|
||||||
return cursor.getBlob(column);
|
byte[] blob = cursor.getBlob(column);
|
||||||
|
String bytes = blob != null ? "(blob) " + Hex.toStringCondensed(Arrays.copyOf(blob, Math.min(blob.length, 32))) : null;
|
||||||
|
if (bytes != null && bytes.length() == 32 && blob.length > 32) {
|
||||||
|
bytes += "...";
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
case Cursor.FIELD_TYPE_STRING:
|
case Cursor.FIELD_TYPE_STRING:
|
||||||
default:
|
default:
|
||||||
return cursor.getString(column);
|
return cursor.getString(column);
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
android:windowSoftInputMode="stateAlwaysHidden"
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity android:name=".recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustResize"/>
|
||||||
|
|
||||||
<activity android:name=".DatabaseMigrationActivity"
|
<activity android:name=".DatabaseMigrationActivity"
|
||||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
@@ -358,8 +363,9 @@
|
|||||||
<activity android:name=".VerifyIdentityActivity"
|
<activity android:name=".VerifyIdentityActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".ApplicationPreferencesActivity"
|
<activity android:name=".components.settings.app.AppSettingsActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -576,6 +582,10 @@
|
|||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||||
android:launchMode="singleTask" />
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
|
<activity android:name=".ratelimit.RecaptchaProofActivity"
|
||||||
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" />
|
||||||
|
|
||||||
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
|
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:theme="@style/TextSecure.FullScreenMedia" />
|
android:theme="@style/TextSecure.FullScreenMedia" />
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 421 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 365 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 664 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 608 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 552 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 631 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 652 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 531 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 685 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 603 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 93 KiB |
1
app/src/main/assets/emoji/emoji_data.json
Normal file
@@ -40,8 +40,10 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
@@ -56,11 +58,10 @@ import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
|||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||||
|
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
|
||||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||||
@@ -154,7 +155,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
|
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
|
||||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||||
|
.addNonBlocking(EmojiSource::refresh)
|
||||||
|
.addNonBlocking(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||||
|
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||||
.addPostRender(this::initializeExpiringMessageManager)
|
.addPostRender(this::initializeExpiringMessageManager)
|
||||||
|
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
|||||||
@@ -1,404 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2011 Whisper Systems
|
|
||||||
* Copyright (C) 2013-2017 Open Whisper Systems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData;
|
|
||||||
import org.thoughtcrime.securesms.help.HelpFragment;
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|
||||||
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
|
|
||||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.DataAndStoragePreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.EditProxyFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
|
||||||
import org.thoughtcrime.securesms.preferences.widgets.PaymentsPreference;
|
|
||||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
|
||||||
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
|
|
||||||
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
|
||||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Activity for application preference display and management.
|
|
||||||
*
|
|
||||||
* @author Moxie Marlinspike
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
|
||||||
{
|
|
||||||
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
|
|
||||||
public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment";
|
|
||||||
public static final String LAUNCH_TO_PROXY_FRAGMENT = "launch.to.proxy.fragment";
|
|
||||||
public static final String LAUNCH_TO_NOTIFICATIONS_FRAGMENT = "launch.to.notifications.fragment";
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static final String TAG = Log.tag(ApplicationPreferencesActivity.class);
|
|
||||||
|
|
||||||
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
|
|
||||||
private static final String PREFERENCE_CATEGORY_USERNAME = "preference_category_username";
|
|
||||||
private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms";
|
|
||||||
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
|
|
||||||
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
|
|
||||||
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
|
|
||||||
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
|
||||||
private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage";
|
|
||||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
|
||||||
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
|
||||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
|
||||||
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
|
||||||
private static final String PREFERENCE_CATEGORY_PAYMENTS = "preference_category_payments";
|
|
||||||
|
|
||||||
private static final String WAS_CONFIGURATION_UPDATED = "was_configuration_updated";
|
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
|
||||||
|
|
||||||
private boolean wasConfigurationUpdated = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreCreate() {
|
|
||||||
dynamicTheme.onCreate(this);
|
|
||||||
dynamicLanguage.onCreate(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle icicle, boolean ready) {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
this.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
|
|
||||||
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
|
|
||||||
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
|
|
||||||
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
|
|
||||||
initFragment(android.R.id.content, new BackupsPreferenceFragment());
|
|
||||||
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_HELP_FRAGMENT, false)) {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putInt(HelpFragment.START_CATEGORY_INDEX, getIntent().getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0));
|
|
||||||
|
|
||||||
initFragment(android.R.id.content, new HelpFragment(), null, bundle);
|
|
||||||
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_PROXY_FRAGMENT, false)) {
|
|
||||||
initFragment(android.R.id.content, EditProxyFragment.newInstance());
|
|
||||||
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_NOTIFICATIONS_FRAGMENT, false)) {
|
|
||||||
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
|
|
||||||
} else if (icicle == null) {
|
|
||||||
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
|
|
||||||
} else {
|
|
||||||
wasConfigurationUpdated = icicle.getBoolean(WAS_CONFIGURATION_UPDATED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
|
||||||
outState.putBoolean(WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated);
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
dynamicTheme.onResume(this);
|
|
||||||
dynamicLanguage.onResume(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data)
|
|
||||||
{
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
Fragment fragment = getSupportFragmentManager().findFragmentById(android.R.id.content);
|
|
||||||
fragment.onActivityResult(requestCode, resultCode, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSupportNavigateUp() {
|
|
||||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
|
||||||
if (fragmentManager.getBackStackEntryCount() > 0) {
|
|
||||||
fragmentManager.popBackStack();
|
|
||||||
} else {
|
|
||||||
if (wasConfigurationUpdated) {
|
|
||||||
setResult(MainActivity.RESULT_CONFIG_CHANGED);
|
|
||||||
} else {
|
|
||||||
setResult(RESULT_OK);
|
|
||||||
}
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
onSupportNavigateUp();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
|
||||||
if (key.equals(TextSecurePreferences.THEME_PREF)) {
|
|
||||||
DynamicTheme.setDefaultDayNightMode(this);
|
|
||||||
recreate();
|
|
||||||
} else if (key.equals(TextSecurePreferences.LANGUAGE_PREF)) {
|
|
||||||
CachedInflater.from(this).clear();
|
|
||||||
wasConfigurationUpdated = true;
|
|
||||||
recreate();
|
|
||||||
|
|
||||||
Intent intent = new Intent(this, KeyCachingService.class);
|
|
||||||
intent.setAction(KeyCachingService.LOCALE_CHANGE_EVENT);
|
|
||||||
startService(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void pushFragment(@NonNull Fragment fragment) {
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
|
||||||
.replace(android.R.id.content, fragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
|
|
||||||
|
|
||||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData = new UnreadPaymentsLiveData();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle icicle) {
|
|
||||||
super.onCreate(icicle);
|
|
||||||
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
|
|
||||||
.setOnPreferenceClickListener(new ProfileClickListener());
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
|
||||||
.setOnPreferenceClickListener(new UsernameClickListener());
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APP_PROTECTION));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_STORAGE)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_STORAGE));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_DONATE)
|
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
|
|
||||||
|
|
||||||
Preference paymentsPreference = this.findPreference(PREFERENCE_CATEGORY_PAYMENTS);
|
|
||||||
|
|
||||||
if (SignalStore.paymentsValues().getPaymentsAvailability().showPaymentsMenu()) {
|
|
||||||
paymentsPreference.setVisible(true);
|
|
||||||
paymentsPreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PAYMENTS));
|
|
||||||
} else {
|
|
||||||
paymentsPreference.setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
tintIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
|
|
||||||
if (SignalStore.paymentsValues().getPaymentsAvailability().showPaymentsMenu()) {
|
|
||||||
PaymentsPreference paymentsPreference = (PaymentsPreference) this.findPreference(PREFERENCE_CATEGORY_PAYMENTS);
|
|
||||||
|
|
||||||
unreadPaymentsLiveData.observe(getViewLifecycleOwner(), unreadPayments -> paymentsPreference.setUnreadCount(unreadPayments.transform(UnreadPayments::getUnreadCount).or(-1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void tintIcons() {
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) return;
|
|
||||||
|
|
||||||
Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS);
|
|
||||||
preference.getIcon().setColorFilter(ContextCompat.getColor(requireContext(), R.color.signal_icon_tint_primary), PorterDuff.Mode.SRC_IN);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
|
||||||
|
|
||||||
if (FeatureFlags.usernames()) {
|
|
||||||
UsernamePreference pref = (UsernamePreference) findPreference(PREFERENCE_CATEGORY_USERNAME);
|
|
||||||
pref.setVisible(shouldDisplayUsernameReminder());
|
|
||||||
pref.setOnLongClickListener(v -> {
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setMessage(R.string.ApplicationPreferencesActivity_hide_reminder)
|
|
||||||
.setPositiveButton(R.string.ApplicationPreferencesActivity_hide, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
SignalStore.misc().hideUsernameReminder();
|
|
||||||
findPreference(PREFERENCE_CATEGORY_USERNAME).setVisible(false);
|
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
|
||||||
.setCancelable(true)
|
|
||||||
.show();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.text_secure_normal__menu_settings);
|
|
||||||
setCategorySummaries();
|
|
||||||
setCategoryVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCategorySummaries() {
|
|
||||||
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
|
|
||||||
|
|
||||||
if (FeatureFlags.usernames()) {
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
|
||||||
.setVisible(shouldDisplayUsernameReminder());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
|
||||||
.setSummary(SmsMmsPreferenceFragment.getSummary(getActivity()));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
|
||||||
.setSummary(NotificationsPreferenceFragment.getSummary(getActivity()));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
|
|
||||||
.setSummary(AppProtectionPreferenceFragment.getSummary(getActivity()));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
|
|
||||||
.setSummary(AppearancePreferenceFragment.getSummary(getActivity()));
|
|
||||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
|
||||||
.setSummary(ChatsPreferenceFragment.getSummary(getActivity()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCategoryVisibility() {
|
|
||||||
Preference devicePreference = this.findPreference(PREFERENCE_CATEGORY_DEVICES);
|
|
||||||
if (devicePreference != null && !TextSecurePreferences.isPushRegistered(getActivity())) {
|
|
||||||
getPreferenceScreen().removePreference(devicePreference);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean shouldDisplayUsernameReminder() {
|
|
||||||
return FeatureFlags.usernames() && !Recipient.self().getUsername().isPresent() && SignalStore.misc().shouldShowUsernameReminder();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
|
||||||
private String category;
|
|
||||||
|
|
||||||
CategoryClickListener(String category) {
|
|
||||||
this.category = category;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
Fragment fragment = null;
|
|
||||||
|
|
||||||
switch (category) {
|
|
||||||
case PREFERENCE_CATEGORY_SMS_MMS:
|
|
||||||
fragment = new SmsMmsPreferenceFragment();
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_NOTIFICATIONS:
|
|
||||||
fragment = new NotificationsPreferenceFragment();
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_APP_PROTECTION:
|
|
||||||
fragment = new AppProtectionPreferenceFragment();
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_APPEARANCE:
|
|
||||||
fragment = new AppearancePreferenceFragment();
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_CHATS:
|
|
||||||
fragment = new ChatsPreferenceFragment();
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_STORAGE:
|
|
||||||
fragment = new DataAndStoragePreferenceFragment();
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_DEVICES:
|
|
||||||
Intent intent = new Intent(getActivity(), DeviceActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_ADVANCED:
|
|
||||||
fragment = new AdvancedPreferenceFragment();
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_HELP:
|
|
||||||
fragment = new HelpFragment();
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_DONATE:
|
|
||||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
|
|
||||||
break;
|
|
||||||
case PREFERENCE_CATEGORY_PAYMENTS:
|
|
||||||
startActivity(new Intent(requireContext(), PaymentsActivity.class));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fragment != null) {
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
|
|
||||||
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
requireActivity().startActivity(ManageProfileActivity.getIntent(requireActivity()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
requireActivity().startActivity(ManageProfileActivity.getIntentForUsernameEdit(preference.getContext()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,10 @@ import androidx.lifecycle.Observer;
|
|||||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||||
|
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
@@ -20,13 +22,14 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public interface BindableConversationItem extends Unbindable {
|
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
||||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||||
@NonNull ConversationMessage messageRecord,
|
@NonNull ConversationMessage messageRecord,
|
||||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||||
@@ -38,7 +41,9 @@ public interface BindableConversationItem extends Unbindable {
|
|||||||
@Nullable String searchQuery,
|
@Nullable String searchQuery,
|
||||||
boolean pulseMention,
|
boolean pulseMention,
|
||||||
boolean hasWallpaper,
|
boolean hasWallpaper,
|
||||||
boolean isMessageRequestAccepted);
|
boolean isMessageRequestAccepted,
|
||||||
|
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||||
|
boolean canPlayInline);
|
||||||
|
|
||||||
ConversationMessage getConversationMessage();
|
ConversationMessage getConversationMessage();
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ public interface BindableConversationItem extends Unbindable {
|
|||||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||||
|
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
|
||||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||||
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||||
void onVoiceNotePause(@NonNull Uri uri);
|
void onVoiceNotePause(@NonNull Uri uri);
|
||||||
@@ -68,6 +74,9 @@ public interface BindableConversationItem extends Unbindable {
|
|||||||
void onJoinGroupCallClicked();
|
void onJoinGroupCallClicked();
|
||||||
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
|
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
|
||||||
void onEnableCallNotificationsClicked();
|
void onEnableCallNotificationsClicked();
|
||||||
|
void onPlayInlineContent(ConversationMessage conversationMessage);
|
||||||
|
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
|
||||||
|
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
|
||||||
|
|
||||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||||
boolean onUrlClicked(@NonNull String url);
|
boolean onUrlClicked(@NonNull String url);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import androidx.annotation.WorkerThread;
|
|||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
@@ -30,15 +32,15 @@ public final class BlockUnblockDialog {
|
|||||||
AlertDialog.Builder::show);
|
AlertDialog.Builder::show);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showBlockAndDeleteFor(@NonNull Context context,
|
public static void showBlockAndReportSpamFor(@NonNull Context context,
|
||||||
@NonNull Lifecycle lifecycle,
|
@NonNull Lifecycle lifecycle,
|
||||||
@NonNull Recipient recipient,
|
@NonNull Recipient recipient,
|
||||||
@NonNull Runnable onBlock,
|
@NonNull Runnable onBlock,
|
||||||
@NonNull Runnable onBlockAndDelete)
|
@NonNull Runnable onBlockAndReportSpam)
|
||||||
{
|
{
|
||||||
SimpleTask.run(lifecycle,
|
SimpleTask.run(lifecycle,
|
||||||
() -> buildBlockFor(context, recipient, onBlock, onBlockAndDelete),
|
() -> buildBlockFor(context, recipient, onBlock, onBlockAndReportSpam),
|
||||||
AlertDialog.Builder::show);
|
AlertDialog.Builder::show);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showUnblockFor(@NonNull Context context,
|
public static void showUnblockFor(@NonNull Context context,
|
||||||
@@ -55,11 +57,11 @@ public final class BlockUnblockDialog {
|
|||||||
private static AlertDialog.Builder buildBlockFor(@NonNull Context context,
|
private static AlertDialog.Builder buildBlockFor(@NonNull Context context,
|
||||||
@NonNull Recipient recipient,
|
@NonNull Recipient recipient,
|
||||||
@NonNull Runnable onBlock,
|
@NonNull Runnable onBlock,
|
||||||
@Nullable Runnable onBlockAndDelete)
|
@Nullable Runnable onBlockAndReportSpam)
|
||||||
{
|
{
|
||||||
recipient = recipient.resolve();
|
recipient = recipient.resolve();
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||||
Resources resources = context.getResources();
|
Resources resources = context.getResources();
|
||||||
|
|
||||||
if (recipient.isGroup()) {
|
if (recipient.isGroup()) {
|
||||||
@@ -78,10 +80,10 @@ public final class BlockUnblockDialog {
|
|||||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||||
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
|
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
|
||||||
|
|
||||||
if (onBlockAndDelete != null) {
|
if (onBlockAndReportSpam != null) {
|
||||||
builder.setNeutralButton(android.R.string.cancel, null);
|
builder.setNeutralButton(android.R.string.cancel, null);
|
||||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_delete, (d, w) -> onBlockAndDelete.run());
|
builder.setNegativeButton(R.string.BlockUnblockDialog_report_spam_and_block, (d, w) -> onBlockAndReportSpam.run());
|
||||||
builder.setNegativeButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run());
|
builder.setPositiveButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run());
|
||||||
} else {
|
} else {
|
||||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
|
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
@@ -98,7 +100,7 @@ public final class BlockUnblockDialog {
|
|||||||
{
|
{
|
||||||
recipient = recipient.resolve();
|
recipient = recipient.resolve();
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||||
Resources resources = context.getResources();
|
Resources resources = context.getResources();
|
||||||
|
|
||||||
if (recipient.isGroup()) {
|
if (recipient.isGroup()) {
|
||||||
|
|||||||
@@ -26,11 +26,12 @@ import org.signal.core.util.logging.Log;
|
|||||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -65,8 +66,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle icicle, boolean ready) {
|
protected void onCreate(Bundle icicle, boolean ready) {
|
||||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||||
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
|
int displayMode = Util.isDefaultSmsProvider(this) ? DisplayMode.FLAG_ALL
|
||||||
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import android.widget.Button;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
@@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
|||||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
@@ -47,7 +49,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
private static final String TAG = Log.tag(DeviceActivity.class);
|
private static final String TAG = Log.tag(DeviceActivity.class);
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||||
|
|
||||||
private DeviceAddFragment deviceAddFragment;
|
private DeviceAddFragment deviceAddFragment;
|
||||||
@@ -62,9 +64,14 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle bundle, boolean ready) {
|
public void onCreate(Bundle bundle, boolean ready) {
|
||||||
getSupportActionBar().setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24));
|
setContentView(R.layout.device_activity);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
getSupportActionBar().setTitle(R.string.AndroidManifest__linked_devices);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
requireSupportActionBar().setTitle(R.string.AndroidManifest__linked_devices);
|
||||||
|
|
||||||
this.deviceAddFragment = new DeviceAddFragment();
|
this.deviceAddFragment = new DeviceAddFragment();
|
||||||
this.deviceListFragment = new DeviceListFragment();
|
this.deviceListFragment = new DeviceListFragment();
|
||||||
this.deviceLinkFragment = new DeviceLinkFragment();
|
this.deviceLinkFragment = new DeviceLinkFragment();
|
||||||
@@ -73,20 +80,10 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
this.deviceAddFragment.setScanListener(this);
|
this.deviceAddFragment.setScanListener(this);
|
||||||
|
|
||||||
if (getIntent().getBooleanExtra("add", false)) {
|
if (getIntent().getBooleanExtra("add", false)) {
|
||||||
initFragment(android.R.id.content, deviceAddFragment, dynamicLanguage.getCurrentLocale());
|
initFragment(R.id.fragment_container, deviceAddFragment, dynamicLanguage.getCurrentLocale());
|
||||||
} else {
|
} else {
|
||||||
initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
initFragment(R.id.fragment_container, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
overridePendingTransition(R.anim.slide_from_end, R.anim.slide_to_start);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
if (isFinishing()) {
|
|
||||||
overridePendingTransition(R.anim.slide_from_start, R.anim.slide_to_end);
|
|
||||||
}
|
|
||||||
super.onPause();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -98,8 +95,9 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
if (item.getItemId() == android.R.id.home) {
|
||||||
case android.R.id.home: finish(); return true;
|
finish();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -113,7 +111,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
|
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.replace(android.R.id.content, deviceAddFragment)
|
.replace(R.id.fragment_container, deviceAddFragment)
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
})
|
})
|
||||||
@@ -128,7 +126,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
Uri uri = Uri.parse(data);
|
Uri uri = Uri.parse(data);
|
||||||
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
|
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
|
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
|
||||||
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
|
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
|
||||||
|
|
||||||
@@ -138,14 +136,14 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
|
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
|
||||||
.replace(android.R.id.content, deviceLinkFragment)
|
.replace(R.id.fragment_container, deviceLinkFragment)
|
||||||
.commit();
|
.commit();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
|
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
|
||||||
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
|
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
|
||||||
.replace(android.R.id.content, deviceLinkFragment)
|
.replace(R.id.fragment_container, deviceLinkFragment)
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ public class DeviceAddFragment extends LoggingFragment {
|
|||||||
this.overlay.setOrientation(LinearLayout.VERTICAL);
|
this.overlay.setOrientation(LinearLayout.VERTICAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
@TargetApi(21)
|
||||||
@Override
|
@Override
|
||||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||||
int oldLeft, int oldTop, int oldRight, int oldBottom)
|
int oldLeft, int oldTop, int oldRight, int oldBottom)
|
||||||
@@ -80,7 +80,7 @@ public class DeviceAddFragment extends LoggingFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
|
||||||
super.onConfigurationChanged(newConfiguration);
|
super.onConfigurationChanged(newConfiguration);
|
||||||
|
|
||||||
this.scannerView.onPause();
|
this.scannerView.onPause();
|
||||||
@@ -107,6 +107,4 @@ public class DeviceAddFragment extends LoggingFragment {
|
|||||||
this.scanningThread.setScanListener(scanListener);
|
this.scanningThread.setScanListener(scanListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
|
||||||
super.onConfigurationChanged(newConfiguration);
|
super.onConfigurationChanged(newConfiguration);
|
||||||
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
container.setOrientation(LinearLayout.HORIZONTAL);
|
container.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import androidx.fragment.app.ListFragment;
|
|||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
import com.melnykov.fab.FloatingActionButton;
|
import com.melnykov.fab.FloatingActionButton;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
@@ -52,12 +53,12 @@ public class DeviceListFragment extends ListFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
|
this.locale = (Locale) requireArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Activity activity) {
|
public void onAttach(@NonNull Context context) {
|
||||||
super.onAttach(activity);
|
super.onAttach(context);
|
||||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,42 +122,22 @@ public class DeviceListFragment extends ListFragment
|
|||||||
final String deviceName = ((DeviceListItem)view).getDeviceName();
|
final String deviceName = ((DeviceListItem)view).getDeviceName();
|
||||||
final long deviceId = ((DeviceListItem)view).getDeviceId();
|
final long deviceId = ((DeviceListItem)view).getDeviceId();
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
|
||||||
builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
|
builder.setTitle(getString(R.string.DeviceListActivity_unlink_s, deviceName));
|
||||||
builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive);
|
builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive);
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> handleDisconnectDevice(deviceId));
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
handleDisconnectDevice(deviceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleLoaderFailed() {
|
private void handleLoaderFailed() {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
|
||||||
builder.setMessage(R.string.DeviceListActivity_network_connection_failed);
|
builder.setMessage(R.string.DeviceListActivity_network_connection_failed);
|
||||||
builder.setPositiveButton(R.string.DeviceListActivity_try_again,
|
builder.setPositiveButton(R.string.DeviceListActivity_try_again,
|
||||||
new DialogInterface.OnClickListener() {
|
(dialog, which) -> getLoaderManager().restartLoader(0, null, DeviceListFragment.this));
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> requireActivity().onBackPressed());
|
||||||
@Override
|
builder.setOnCancelListener(dialog -> requireActivity().onBackPressed());
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
DeviceListFragment.this.getActivity().onBackPressed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
|
||||||
@Override
|
|
||||||
public void onCancel(DialogInterface dialog) {
|
|
||||||
DeviceListFragment.this.getActivity().onBackPressed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
|
|
||||||
|
|
||||||
public class ExpirationDialog extends AlertDialog {
|
|
||||||
|
|
||||||
protected ExpirationDialog(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ExpirationDialog(Context context, int theme) {
|
|
||||||
super(context, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ExpirationDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
|
|
||||||
super(context, cancelable, cancelListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void show(final Context context,
|
|
||||||
final int currentExpiration,
|
|
||||||
final @NonNull OnClickListener listener)
|
|
||||||
{
|
|
||||||
final View view = createNumberPickerView(context, currentExpiration);
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
|
||||||
builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
|
|
||||||
builder.setView(view);
|
|
||||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
|
||||||
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
|
|
||||||
listener.onClick(getExpirationTimes(context, currentExpiration)[selected]);
|
|
||||||
});
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static View createNumberPickerView(final Context context, final int currentExpiration) {
|
|
||||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
|
||||||
final View view = inflater.inflate(R.layout.expiration_dialog, null);
|
|
||||||
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
|
|
||||||
final TextView textView = view.findViewById(R.id.expiration_details);
|
|
||||||
final int[] expirationTimes = getExpirationTimes(context, currentExpiration);
|
|
||||||
final String[] expirationDisplayValues = new String[expirationTimes.length];
|
|
||||||
|
|
||||||
int selectedIndex = expirationTimes.length - 1;
|
|
||||||
|
|
||||||
for (int i=0;i<expirationTimes.length;i++) {
|
|
||||||
expirationDisplayValues[i] = ExpirationUtil.getExpirationDisplayValue(context, expirationTimes[i]);
|
|
||||||
|
|
||||||
if ((currentExpiration >= expirationTimes[i]) &&
|
|
||||||
(i == expirationTimes.length -1 || currentExpiration < expirationTimes[i+1])) {
|
|
||||||
selectedIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
numberPickerView.setDisplayedValues(expirationDisplayValues);
|
|
||||||
numberPickerView.setMinValue(0);
|
|
||||||
numberPickerView.setMaxValue(expirationTimes.length-1);
|
|
||||||
|
|
||||||
NumberPickerView.OnValueChangeListener listener = (picker, oldVal, newVal) -> {
|
|
||||||
if (newVal == 0) {
|
|
||||||
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
|
|
||||||
} else {
|
|
||||||
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
numberPickerView.setOnValueChangedListener(listener);
|
|
||||||
numberPickerView.setValue(selectedIndex);
|
|
||||||
listener.onValueChange(numberPickerView, selectedIndex, selectedIndex);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int[] getExpirationTimes(Context context, int currentExpiration) {
|
|
||||||
int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
|
|
||||||
int location = Arrays.binarySearch(expirationTimes, currentExpiration);
|
|
||||||
if (location < 0) {
|
|
||||||
int[] temp = Arrays.copyOf(expirationTimes, expirationTimes.length + 1);
|
|
||||||
temp[temp.length - 1] = currentExpiration;
|
|
||||||
Arrays.sort(temp);
|
|
||||||
expirationTimes = temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return expirationTimes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnClickListener {
|
|
||||||
public void onClick(int expirationTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity;
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
|
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
|
||||||
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
|
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
|
||||||
@@ -69,8 +71,7 @@ public class MainNavigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void goToAppSettings() {
|
public void goToAppSettings() {
|
||||||
Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
|
activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES);
|
||||||
activity.startActivityForResult(intent, REQUEST_CONFIG_CHANGES);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void goToArchiveList() {
|
public void goToArchiveList() {
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
|||||||
return STATE_UI_BLOCKING_UPGRADE;
|
return STATE_UI_BLOCKING_UPGRADE;
|
||||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||||
return STATE_WELCOME_PUSH_SCREEN;
|
return STATE_WELCOME_PUSH_SCREEN;
|
||||||
} else if (SignalStore.storageServiceValues().needsAccountRestore()) {
|
} else if (SignalStore.storageService().needsAccountRestore()) {
|
||||||
return STATE_ENTER_SIGNAL_PIN;
|
return STATE_ENTER_SIGNAL_PIN;
|
||||||
} else if (userMustSetProfileName()) {
|
} else if (userMustSetProfileName()) {
|
||||||
return STATE_CREATE_PROFILE_NAME;
|
return STATE_CREATE_PROFILE_NAME;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public abstract class Attachment {
|
|||||||
|
|
||||||
private final boolean voiceNote;
|
private final boolean voiceNote;
|
||||||
private final boolean borderless;
|
private final boolean borderless;
|
||||||
|
private final boolean videoGif;
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
private final int height;
|
||||||
private final boolean quote;
|
private final boolean quote;
|
||||||
@@ -72,6 +73,7 @@ public abstract class Attachment {
|
|||||||
@Nullable String fastPreflightId,
|
@Nullable String fastPreflightId,
|
||||||
boolean voiceNote,
|
boolean voiceNote,
|
||||||
boolean borderless,
|
boolean borderless,
|
||||||
|
boolean videoGif,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
boolean quote,
|
boolean quote,
|
||||||
@@ -94,6 +96,7 @@ public abstract class Attachment {
|
|||||||
this.fastPreflightId = fastPreflightId;
|
this.fastPreflightId = fastPreflightId;
|
||||||
this.voiceNote = voiceNote;
|
this.voiceNote = voiceNote;
|
||||||
this.borderless = borderless;
|
this.borderless = borderless;
|
||||||
|
this.videoGif = videoGif;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.quote = quote;
|
this.quote = quote;
|
||||||
@@ -170,6 +173,10 @@ public abstract class Attachment {
|
|||||||
return borderless;
|
return borderless;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isVideoGif() {
|
||||||
|
return videoGif;
|
||||||
|
}
|
||||||
|
|
||||||
public int getWidth() {
|
public int getWidth() {
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class DatabaseAttachment extends Attachment {
|
|||||||
String fastPreflightId,
|
String fastPreflightId,
|
||||||
boolean voiceNote,
|
boolean voiceNote,
|
||||||
boolean borderless,
|
boolean borderless,
|
||||||
|
boolean videoGif,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
boolean quote,
|
boolean quote,
|
||||||
@@ -47,7 +48,7 @@ public class DatabaseAttachment extends Attachment {
|
|||||||
int displayOrder,
|
int displayOrder,
|
||||||
long uploadTimestamp)
|
long uploadTimestamp)
|
||||||
{
|
{
|
||||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||||
this.attachmentId = attachmentId;
|
this.attachmentId = attachmentId;
|
||||||
this.hasData = hasData;
|
this.hasData = hasData;
|
||||||
this.hasThumbnail = hasThumbnail;
|
this.hasThumbnail = hasThumbnail;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
|||||||
public class MmsNotificationAttachment extends Attachment {
|
public class MmsNotificationAttachment extends Attachment {
|
||||||
|
|
||||||
public MmsNotificationAttachment(int status, long size) {
|
public MmsNotificationAttachment(int status, long size) {
|
||||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, 0, 0, false, 0, null, null, null, null, null);
|
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class PointerAttachment extends Attachment {
|
|||||||
@Nullable String fastPreflightId,
|
@Nullable String fastPreflightId,
|
||||||
boolean voiceNote,
|
boolean voiceNote,
|
||||||
boolean borderless,
|
boolean borderless,
|
||||||
|
boolean videoGif,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
long uploadTimestamp,
|
long uploadTimestamp,
|
||||||
@@ -37,7 +38,7 @@ public class PointerAttachment extends Attachment {
|
|||||||
@Nullable StickerLocator stickerLocator,
|
@Nullable StickerLocator stickerLocator,
|
||||||
@Nullable BlurHash blurHash)
|
@Nullable BlurHash blurHash)
|
||||||
{
|
{
|
||||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -111,6 +112,7 @@ public class PointerAttachment extends Attachment {
|
|||||||
fastPreflightId,
|
fastPreflightId,
|
||||||
pointer.get().asPointer().getVoiceNote(),
|
pointer.get().asPointer().getVoiceNote(),
|
||||||
pointer.get().asPointer().isBorderless(),
|
pointer.get().asPointer().isBorderless(),
|
||||||
|
pointer.get().asPointer().isGif(),
|
||||||
pointer.get().asPointer().getWidth(),
|
pointer.get().asPointer().getWidth(),
|
||||||
pointer.get().asPointer().getHeight(),
|
pointer.get().asPointer().getHeight(),
|
||||||
pointer.get().asPointer().getUploadTimestamp(),
|
pointer.get().asPointer().getUploadTimestamp(),
|
||||||
@@ -135,6 +137,7 @@ public class PointerAttachment extends Attachment {
|
|||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
|
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
|
||||||
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
|
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
|
||||||
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
|
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|||||||
public class TombstoneAttachment extends Attachment {
|
public class TombstoneAttachment extends Attachment {
|
||||||
|
|
||||||
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
||||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, 0, 0, quote, 0, null, null, null, null, null);
|
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class UriAttachment extends Attachment {
|
|||||||
@Nullable String fileName,
|
@Nullable String fileName,
|
||||||
boolean voiceNote,
|
boolean voiceNote,
|
||||||
boolean borderless,
|
boolean borderless,
|
||||||
|
boolean videoGif,
|
||||||
boolean quote,
|
boolean quote,
|
||||||
@Nullable String caption,
|
@Nullable String caption,
|
||||||
@Nullable StickerLocator stickerLocator,
|
@Nullable StickerLocator stickerLocator,
|
||||||
@@ -28,7 +29,7 @@ public class UriAttachment extends Attachment {
|
|||||||
@Nullable AudioHash audioHash,
|
@Nullable AudioHash audioHash,
|
||||||
@Nullable TransformProperties transformProperties)
|
@Nullable TransformProperties transformProperties)
|
||||||
{
|
{
|
||||||
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, videoGif, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UriAttachment(@NonNull Uri dataUri,
|
public UriAttachment(@NonNull Uri dataUri,
|
||||||
@@ -41,6 +42,7 @@ public class UriAttachment extends Attachment {
|
|||||||
@Nullable String fastPreflightId,
|
@Nullable String fastPreflightId,
|
||||||
boolean voiceNote,
|
boolean voiceNote,
|
||||||
boolean borderless,
|
boolean borderless,
|
||||||
|
boolean videoGif,
|
||||||
boolean quote,
|
boolean quote,
|
||||||
@Nullable String caption,
|
@Nullable String caption,
|
||||||
@Nullable StickerLocator stickerLocator,
|
@Nullable StickerLocator stickerLocator,
|
||||||
@@ -48,7 +50,7 @@ public class UriAttachment extends Attachment {
|
|||||||
@Nullable AudioHash audioHash,
|
@Nullable AudioHash audioHash,
|
||||||
@Nullable TransformProperties transformProperties)
|
@Nullable TransformProperties transformProperties)
|
||||||
{
|
{
|
||||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||||
this.dataUri = dataUri;
|
this.dataUri = dataUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class BackupDialog {
|
|||||||
|
|
||||||
BackupPassphrase.set(context, Util.join(password, " "));
|
BackupPassphrase.set(context, Util.join(password, " "));
|
||||||
TextSecurePreferences.setNextBackupTime(context, 0);
|
TextSecurePreferences.setNextBackupTime(context, 0);
|
||||||
TextSecurePreferences.setBackupEnabled(context, true);
|
SignalStore.settings().setBackupEnabled(true);
|
||||||
LocalBackupListener.schedule(context);
|
LocalBackupListener.schedule(context);
|
||||||
|
|
||||||
onBackupsEnabled.run();
|
onBackupsEnabled.run();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.backup;
|
|||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -11,8 +10,8 @@ import androidx.annotation.StringRes;
|
|||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper;
|
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
|
|
||||||
@@ -39,11 +38,7 @@ public enum BackupFileIOError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void postNotification(@NonNull Context context) {
|
public void postNotification(@NonNull Context context) {
|
||||||
Intent intent = new Intent(context, ApplicationPreferencesActivity.class);
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), 0);
|
||||||
|
|
||||||
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_BACKUPS_FRAGMENT, true);
|
|
||||||
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, intent, 0);
|
|
||||||
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
|
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
|
||||||
.setSmallIcon(R.drawable.ic_signal_backup)
|
.setSmallIcon(R.drawable.ic_signal_backup)
|
||||||
.setContentTitle(context.getString(titleId))
|
.setContentTitle(context.getString(titleId))
|
||||||
|
|||||||
@@ -69,4 +69,10 @@ public class AlertView extends LinearLayout {
|
|||||||
approvalIndicator.setVisibility(View.GONE);
|
approvalIndicator.setVisibility(View.GONE);
|
||||||
failedIndicator.setVisibility(View.VISIBLE);
|
failedIndicator.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRateLimited() {
|
||||||
|
this.setVisibility(View.VISIBLE);
|
||||||
|
approvalIndicator.setVisibility(View.VISIBLE);
|
||||||
|
failedIndicator.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
@@ -14,7 +15,11 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.appcompat.widget.AppCompatImageView;
|
import androidx.appcompat.widget.AppCompatImageView;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.MultiTransformation;
|
||||||
|
import com.bumptech.glide.load.Transformation;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
@@ -23,6 +28,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
|||||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
@@ -30,9 +36,12 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||||
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
|
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
|
||||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class AvatarImageView extends AppCompatImageView {
|
public final class AvatarImageView extends AppCompatImageView {
|
||||||
@@ -63,6 +72,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
private Paint outlinePaint;
|
private Paint outlinePaint;
|
||||||
private OnClickListener listener;
|
private OnClickListener listener;
|
||||||
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
|
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
|
||||||
|
private boolean blurred;
|
||||||
|
|
||||||
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
||||||
private @NonNull Drawable unknownRecipientDrawable;
|
private @NonNull Drawable unknownRecipientDrawable;
|
||||||
@@ -90,15 +100,16 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
|
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
|
||||||
|
|
||||||
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
|
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
|
||||||
|
blurred = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDraw(Canvas canvas) {
|
protected void onDraw(Canvas canvas) {
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
|
|
||||||
float width = getWidth() - getPaddingRight() - getPaddingLeft();
|
float width = getWidth() - getPaddingRight() - getPaddingLeft();
|
||||||
float height = getHeight() - getPaddingBottom() - getPaddingTop();
|
float height = getHeight() - getPaddingBottom() - getPaddingTop();
|
||||||
float cx = width / 2f;
|
float cx = width / 2f;
|
||||||
float cy = height / 2f;
|
float cy = height / 2f;
|
||||||
float radius = Math.min(cx, cy) - (outlinePaint.getStrokeWidth() / 2f);
|
float radius = Math.min(cx, cy) - (outlinePaint.getStrokeWidth() / 2f);
|
||||||
|
|
||||||
@@ -160,20 +171,30 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
Recipient.self().getProfileAvatar()))
|
Recipient.self().getProfileAvatar()))
|
||||||
: new RecipientContactPhoto(recipient);
|
: new RecipientContactPhoto(recipient);
|
||||||
|
|
||||||
if (!photo.equals(recipientContactPhoto)) {
|
boolean shouldBlur = recipient.shouldBlurAvatar();
|
||||||
|
|
||||||
|
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred) {
|
||||||
requestManager.clear(this);
|
requestManager.clear(this);
|
||||||
recipientContactPhoto = photo;
|
recipientContactPhoto = photo;
|
||||||
|
|
||||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL
|
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
|
||||||
? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
|
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
|
||||||
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
|
|
||||||
|
|
||||||
if (photo.contactPhoto != null) {
|
if (photo.contactPhoto != null) {
|
||||||
|
|
||||||
|
List<Transformation<Bitmap>> transforms = new ArrayList<>();
|
||||||
|
if (shouldBlur) {
|
||||||
|
transforms.add(new BlurTransformation(ApplicationDependencies.getApplication(), 0.25f, BlurTransformation.MAX_RADIUS));
|
||||||
|
}
|
||||||
|
transforms.add(new CircleCrop());
|
||||||
|
blurred = shouldBlur;
|
||||||
|
|
||||||
requestManager.load(photo.contactPhoto)
|
requestManager.load(photo.contactPhoto)
|
||||||
.fallback(fallbackContactPhotoDrawable)
|
.fallback(fallbackContactPhotoDrawable)
|
||||||
.error(fallbackContactPhotoDrawable)
|
.error(fallbackContactPhotoDrawable)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
.circleCrop()
|
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||||
|
.transform(new MultiTransformation<>(transforms))
|
||||||
.into(this);
|
.into(this);
|
||||||
} else {
|
} else {
|
||||||
setImageDrawable(fallbackContactPhotoDrawable);
|
setImageDrawable(fallbackContactPhotoDrawable);
|
||||||
@@ -191,7 +212,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
setImageDrawable(unknownRecipientDrawable);
|
setImageDrawable(unknownRecipientDrawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
super.setOnClickListener(listener);
|
disableQuickContact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.components.mention.MentionDeleter;
|
|||||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||||
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
|
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
|
||||||
import org.thoughtcrime.securesms.database.model.Mention;
|
import org.thoughtcrime.securesms.database.model.Mention;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.StringUtil;
|
import org.thoughtcrime.securesms.util.StringUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
@@ -201,7 +202,7 @@ public class ComposeText extends EmojiEditText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setTransport(TransportOption transport) {
|
public void setTransport(TransportOption transport) {
|
||||||
final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext());
|
final boolean useSystemEmoji = SignalStore.settings().isPreferSystemEmoji();
|
||||||
|
|
||||||
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
|
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
|
||||||
int inputType = getInputType();
|
int inputType = getInputType();
|
||||||
@@ -225,7 +226,7 @@ public class ComposeText extends EmojiEditText {
|
|||||||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||||
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
|
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
|
||||||
|
|
||||||
if(TextSecurePreferences.isEnterSendsEnabled(getContext())) {
|
if(SignalStore.settings().isEnterKeySends()) {
|
||||||
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public class ConversationItemFooter extends LinearLayout {
|
|||||||
presentTimer(messageRecord);
|
presentTimer(messageRecord);
|
||||||
presentInsecureIndicator(messageRecord);
|
presentInsecureIndicator(messageRecord);
|
||||||
presentDeliveryStatus(messageRecord);
|
presentDeliveryStatus(messageRecord);
|
||||||
hideAudioDurationViews();
|
presentAudioDuration(messageRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
|
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
|
||||||
@@ -161,6 +161,8 @@ public class ConversationItemFooter extends LinearLayout {
|
|||||||
dateView.setText(errorMsg);
|
dateView.setText(errorMsg);
|
||||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||||
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
||||||
|
} else if (messageRecord.isRateLimited()) {
|
||||||
|
dateView.setText(R.string.ConversationItem_send_paused);
|
||||||
} else {
|
} else {
|
||||||
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
||||||
}
|
}
|
||||||
@@ -259,6 +261,12 @@ public class ConversationItemFooter extends LinearLayout {
|
|||||||
moveAudioViewsForIncoming();
|
moveAudioViewsForIncoming();
|
||||||
}
|
}
|
||||||
showAudioDurationViews();
|
showAudioDurationViews();
|
||||||
|
|
||||||
|
if (messageRecord.getViewedReceiptCount() > 0) {
|
||||||
|
revealDot.setProgress(1f);
|
||||||
|
} else {
|
||||||
|
revealDot.setProgress(0f);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
hideAudioDurationViews();
|
hideAudioDurationViews();
|
||||||
}
|
}
|
||||||
@@ -295,7 +303,7 @@ public class ConversationItemFooter extends LinearLayout {
|
|||||||
|
|
||||||
private void showAudioDurationViews() {
|
private void showAudioDurationViews() {
|
||||||
audioSpace.setVisibility(View.VISIBLE);
|
audioSpace.setVisibility(View.VISIBLE);
|
||||||
audioDuration.setVisibility(View.VISIBLE);
|
audioDuration.setVisibility(View.GONE);
|
||||||
|
|
||||||
if (FeatureFlags.viewedReceipts()) {
|
if (FeatureFlags.viewedReceipts()) {
|
||||||
revealDot.setVisibility(View.VISIBLE);
|
revealDot.setVisibility(View.VISIBLE);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ public class ConversationItemThumbnail extends FrameLayout {
|
|||||||
private Outliner outliner;
|
private Outliner outliner;
|
||||||
private Outliner pulseOutliner;
|
private Outliner pulseOutliner;
|
||||||
private boolean borderless;
|
private boolean borderless;
|
||||||
|
private int[] normalBounds;
|
||||||
|
private int[] gifBounds;
|
||||||
|
private int minimumThumbnailWidth;
|
||||||
|
|
||||||
public ConversationItemThumbnail(Context context) {
|
public ConversationItemThumbnail(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@@ -60,14 +64,30 @@ public class ConversationItemThumbnail extends FrameLayout {
|
|||||||
|
|
||||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||||
|
|
||||||
|
int gifWidth = ViewUtil.dpToPx(260);
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
||||||
thumbnail.setBounds(typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
|
normalBounds = new int[]{
|
||||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
|
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
|
||||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
|
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
|
||||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0));
|
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
|
||||||
|
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
gifWidth = typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_gifWidth, gifWidth);
|
||||||
typedArray.recycle();
|
typedArray.recycle();
|
||||||
|
} else {
|
||||||
|
normalBounds = new int[]{0, 0, 0, 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gifBounds = new int[]{
|
||||||
|
gifWidth,
|
||||||
|
gifWidth,
|
||||||
|
1,
|
||||||
|
Integer.MAX_VALUE
|
||||||
|
};
|
||||||
|
|
||||||
|
minimumThumbnailWidth = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
@SuppressWarnings("SuspiciousNameCombination")
|
||||||
@@ -88,6 +108,18 @@ public class ConversationItemThumbnail extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void hideThumbnailView() {
|
||||||
|
thumbnail.setAlpha(0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showThumbnailView() {
|
||||||
|
thumbnail.setAlpha(1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable CornerMask getCornerMask() {
|
||||||
|
return cornerMask;
|
||||||
|
}
|
||||||
|
|
||||||
public void setPulseOutliner(@NonNull Outliner outliner) {
|
public void setPulseOutliner(@NonNull Outliner outliner) {
|
||||||
this.pulseOutliner = outliner;
|
this.pulseOutliner = outliner;
|
||||||
}
|
}
|
||||||
@@ -121,6 +153,7 @@ public class ConversationItemThumbnail extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setMinimumThumbnailWidth(int width) {
|
public void setMinimumThumbnailWidth(int width) {
|
||||||
|
minimumThumbnailWidth = width;
|
||||||
thumbnail.setMinimumThumbnailWidth(width);
|
thumbnail.setMinimumThumbnailWidth(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +170,17 @@ public class ConversationItemThumbnail extends FrameLayout {
|
|||||||
boolean showControls, boolean isPreview)
|
boolean showControls, boolean isPreview)
|
||||||
{
|
{
|
||||||
if (slides.size() == 1) {
|
if (slides.size() == 1) {
|
||||||
|
Slide slide = slides.get(0);
|
||||||
|
if (slide.isVideoGif()) {
|
||||||
|
setThumbnailBounds(gifBounds);
|
||||||
|
} else {
|
||||||
|
setThumbnailBounds(normalBounds);
|
||||||
|
|
||||||
|
if (minimumThumbnailWidth != -1) {
|
||||||
|
thumbnail.setMinimumThumbnailWidth(minimumThumbnailWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
thumbnail.setVisibility(VISIBLE);
|
thumbnail.setVisibility(VISIBLE);
|
||||||
album.setVisibility(GONE);
|
album.setVisibility(GONE);
|
||||||
|
|
||||||
@@ -167,4 +211,8 @@ public class ConversationItemThumbnail extends FrameLayout {
|
|||||||
thumbnail.setDownloadClickListener(listener);
|
thumbnail.setDownloadClickListener(listener);
|
||||||
album.setDownloadClickListener(listener);
|
album.setDownloadClickListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setThumbnailBounds(@NonNull int[] bounds) {
|
||||||
|
thumbnail.setBounds(bounds[0], bounds[1], bounds[2], bounds[3]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import android.graphics.Path;
|
|||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.graphics.PorterDuffXfermode;
|
import android.graphics.PorterDuffXfermode;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class CornerMask {
|
public class CornerMask {
|
||||||
|
|
||||||
@@ -20,19 +22,24 @@ public class CornerMask {
|
|||||||
private final RectF bounds = new RectF();
|
private final RectF bounds = new RectF();
|
||||||
|
|
||||||
public CornerMask(@NonNull View view) {
|
public CornerMask(@NonNull View view) {
|
||||||
|
this(view, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CornerMask(@NonNull View view, @Nullable CornerMask toClone) {
|
||||||
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||||
|
|
||||||
clearPaint.setColor(Color.BLACK);
|
clearPaint.setColor(Color.BLACK);
|
||||||
clearPaint.setStyle(Paint.Style.FILL);
|
clearPaint.setStyle(Paint.Style.FILL);
|
||||||
clearPaint.setAntiAlias(true);
|
clearPaint.setAntiAlias(true);
|
||||||
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||||
|
|
||||||
|
if (toClone != null) {
|
||||||
|
System.arraycopy(toClone.radii, 0, radii, 0, radii.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mask(Canvas canvas) {
|
public void mask(Canvas canvas) {
|
||||||
bounds.left = 0;
|
bounds.set(canvas.getClipBounds());
|
||||||
bounds.top = 0;
|
|
||||||
bounds.right = canvas.getWidth();
|
|
||||||
bounds.bottom = canvas.getHeight();
|
|
||||||
|
|
||||||
corners.reset();
|
corners.reset();
|
||||||
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
||||||
@@ -72,4 +79,8 @@ public class CornerMask {
|
|||||||
public void setBottomLeftRadius(int radius) {
|
public void setBottomLeftRadius(int radius) {
|
||||||
radii[6] = radii[7] = radius;
|
radii[6] = radii[7] = radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float[] getRadii() {
|
||||||
|
return radii;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
|||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
|
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
|
||||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
@@ -124,7 +125,7 @@ public class InputPanel extends LinearLayout
|
|||||||
|
|
||||||
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
|
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
|
||||||
|
|
||||||
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
|
if (SignalStore.settings().isPreferSystemEmoji()) {
|
||||||
mediaKeyboard.setVisibility(View.GONE);
|
mediaKeyboard.setVisibility(View.GONE);
|
||||||
emojiVisible = false;
|
emojiVisible = false;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Insets;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.os.Build;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.constraintlayout.widget.Guideline;
|
import androidx.constraintlayout.widget.Guideline;
|
||||||
|
|
||||||
|
import org.signal.glide.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
|
||||||
@@ -26,12 +31,31 @@ public class InsetAwareConstraintLayout extends ConstraintLayout {
|
|||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
@Override
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
@TargetApi(20)
|
||||||
|
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
|
||||||
|
if (Build.VERSION.SDK_INT < 30) {
|
||||||
|
return super.onApplyWindowInsets(insets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Insets windowInsets = insets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.ime() | WindowInsets.Type.displayCutout());
|
||||||
|
applyInsets(new Rect(windowInsets.left, windowInsets.top, windowInsets.right, windowInsets.bottom));
|
||||||
|
|
||||||
|
return super.onApplyWindowInsets(insets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean fitSystemWindows(Rect insets) {
|
protected boolean fitSystemWindows(Rect insets) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyInsets(insets);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyInsets(@NonNull Rect insets) {
|
||||||
Guideline statusBarGuideline = findViewById(R.id.status_bar_guideline);
|
Guideline statusBarGuideline = findViewById(R.id.status_bar_guideline);
|
||||||
Guideline navigationBarGuideline = findViewById(R.id.navigation_bar_guideline);
|
Guideline navigationBarGuideline = findViewById(R.id.navigation_bar_guideline);
|
||||||
Guideline parentStartGuideline = findViewById(R.id.parent_start_guideline);
|
Guideline parentStartGuideline = findViewById(R.id.parent_start_guideline);
|
||||||
@@ -60,7 +84,5 @@ public class InsetAwareConstraintLayout extends ConstraintLayout {
|
|||||||
parentEndGuideline.setGuidelineEnd(insets.left);
|
parentEndGuideline.setGuidelineEnd(insets.left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ import android.view.ViewTreeObserver;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class MaskView extends View {
|
public class MaskView extends View {
|
||||||
|
|
||||||
private View target;
|
private MaskTarget maskTarget;
|
||||||
private ViewGroup activityContentView;
|
private ViewGroup activityContentView;
|
||||||
private Paint maskPaint;
|
private Paint maskPaint;
|
||||||
private Rect drawingRect = new Rect();
|
private Rect drawingRect = new Rect();
|
||||||
private float targetParentTranslationY;
|
private float targetParentTranslationY;
|
||||||
|
|
||||||
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
|
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
|
||||||
|
|
||||||
@@ -50,15 +54,15 @@ public class MaskView extends View {
|
|||||||
activityContentView = getRootView().findViewById(android.R.id.content);
|
activityContentView = getRootView().findViewById(android.R.id.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTarget(@Nullable View target) {
|
public void setTarget(@Nullable MaskTarget maskTarget) {
|
||||||
if (this.target != null) {
|
if (this.maskTarget != null) {
|
||||||
this.target.getViewTreeObserver().removeOnDrawListener(onDrawListener);
|
removeOnDrawListener(this.maskTarget, onDrawListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.target = target;
|
this.maskTarget = maskTarget;
|
||||||
|
|
||||||
if (this.target != null) {
|
if (this.maskTarget != null) {
|
||||||
this.target.getViewTreeObserver().addOnDrawListener(onDrawListener);
|
addOnDrawListener(maskTarget, onDrawListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
@@ -72,26 +76,77 @@ public class MaskView extends View {
|
|||||||
protected void onDraw(@NonNull Canvas canvas) {
|
protected void onDraw(@NonNull Canvas canvas) {
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
|
|
||||||
if (target == null || !target.isAttachedToWindow()) {
|
if (nothingToMask(maskTarget)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
target.getDrawingRect(drawingRect);
|
maskTarget.getPrimaryTarget().getDrawingRect(drawingRect);
|
||||||
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
|
activityContentView.offsetDescendantRectToMyCoords(maskTarget.getPrimaryTarget(), drawingRect);
|
||||||
|
|
||||||
drawingRect.top += targetParentTranslationY;
|
drawingRect.top += targetParentTranslationY;
|
||||||
drawingRect.bottom += targetParentTranslationY;
|
drawingRect.bottom += targetParentTranslationY;
|
||||||
|
|
||||||
Bitmap mask = Bitmap.createBitmap(target.getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
|
Bitmap mask = Bitmap.createBitmap(maskTarget.getPrimaryTarget().getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
|
||||||
Canvas maskCanvas = new Canvas(mask);
|
Canvas maskCanvas = new Canvas(mask);
|
||||||
|
|
||||||
target.draw(maskCanvas);
|
maskTarget.draw(maskCanvas);
|
||||||
|
|
||||||
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
||||||
|
|
||||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) target.getLayoutParams();
|
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) maskTarget.getPrimaryTarget().getLayoutParams();
|
||||||
canvas.drawBitmap(mask, params.leftMargin, drawingRect.top, maskPaint);
|
canvas.drawBitmap(mask, params.leftMargin, drawingRect.top, maskPaint);
|
||||||
|
|
||||||
mask.recycle();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import org.thoughtcrime.securesms.util.Util;
|
|||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||||
|
import org.thoughtcrime.securesms.util.views.Stub;
|
||||||
|
import org.thoughtcrime.securesms.video.VideoPlayer;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -55,11 +57,12 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
private static final int MIN_HEIGHT = 2;
|
private static final int MIN_HEIGHT = 2;
|
||||||
private static final int MAX_HEIGHT = 3;
|
private static final int MAX_HEIGHT = 3;
|
||||||
|
|
||||||
private ImageView image;
|
private ImageView image;
|
||||||
private ImageView blurhash;
|
private ImageView blurhash;
|
||||||
private View playOverlay;
|
private View playOverlay;
|
||||||
private View captionIcon;
|
private View captionIcon;
|
||||||
private OnClickListener parentClickListener;
|
private Stub<VideoPlayer> videoPlayer;
|
||||||
|
private OnClickListener parentClickListener;
|
||||||
|
|
||||||
private final int[] dimens = new int[2];
|
private final int[] dimens = new int[2];
|
||||||
private final int[] bounds = new int[4];
|
private final int[] bounds = new int[4];
|
||||||
@@ -90,6 +93,7 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
this.blurhash = findViewById(R.id.thumbnail_blurhash);
|
this.blurhash = findViewById(R.id.thumbnail_blurhash);
|
||||||
this.playOverlay = findViewById(R.id.play_overlay);
|
this.playOverlay = findViewById(R.id.play_overlay);
|
||||||
this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
|
this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
|
||||||
|
this.videoPlayer = new Stub<>(findViewById(R.id.thumbnail_player_stub));
|
||||||
super.setOnClickListener(new ThumbnailClickDispatcher());
|
super.setOnClickListener(new ThumbnailClickDispatcher());
|
||||||
|
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
@@ -335,6 +339,7 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result));
|
buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result));
|
||||||
|
|
||||||
resultHandled = true;
|
resultHandled = true;
|
||||||
} else {
|
} else {
|
||||||
glideRequests.clear(image);
|
glideRequests.clear(image);
|
||||||
@@ -442,7 +447,7 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
request = request.override(size[WIDTH], size[HEIGHT]);
|
request = request.override(size[WIDTH], size[HEIGHT]);
|
||||||
|
|
||||||
if (radius > 0) {
|
if (radius > 0) {
|
||||||
return request.transforms(fitting, new RoundedCorners(radius));
|
return request.transforms(fitting, new RoundedCorners(radius));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.AttrRes;
|
import androidx.annotation.AttrRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class CompositeEmojiPageModel implements EmojiPageModel {
|
public class CompositeEmojiPageModel implements EmojiPageModel {
|
||||||
@AttrRes private final int iconAttr;
|
@AttrRes private final int iconAttr;
|
||||||
@NonNull private final EmojiPageModel[] models;
|
@NonNull private final List<EmojiPageModel> models;
|
||||||
|
|
||||||
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull EmojiPageModel... models) {
|
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List<EmojiPageModel> models) {
|
||||||
this.iconAttr = iconAttr;
|
this.iconAttr = iconAttr;
|
||||||
this.models = models;
|
this.models = models;
|
||||||
}
|
}
|
||||||
@@ -39,12 +42,7 @@ public class CompositeEmojiPageModel implements EmojiPageModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasSpriteMap() {
|
public @Nullable Uri getSpriteUri() {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable String getSprite() {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ public class Emoji {
|
|||||||
this.variations = Arrays.asList(variations);
|
this.variations = Arrays.asList(variations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Emoji(List<String> variations) {
|
||||||
|
this.variations = variations;
|
||||||
|
}
|
||||||
|
|
||||||
public String getValue() {
|
public String getValue() {
|
||||||
return variations.get(0);
|
return variations.get(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.appcompat.widget.AppCompatEditText;
|
|||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
|
|
||||||
@@ -34,8 +35,10 @@ public class EmojiEditText extends AppCompatEditText {
|
|||||||
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||||
a.recycle();
|
a.recycle();
|
||||||
|
|
||||||
if (forceCustom || !TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
|
if (forceCustom || !SignalStore.settings().isPreferSystemEmoji()) {
|
||||||
setFilters(appendEmojiFilter(this.getFilters()));
|
if (!isInEditMode()) {
|
||||||
|
setFilters(appendEmojiFilter(this.getFilters()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class EmojiFilter implements InputFilter {
|
|||||||
char[] v = new char[end - start];
|
char[] v = new char[end - start];
|
||||||
TextUtils.getChars(source, start, end, v, 0);
|
TextUtils.getChars(source, start, end, v, 0);
|
||||||
|
|
||||||
Spannable emojified = EmojiProvider.getInstance(view.getContext()).emojify(new String(v), view);
|
Spannable emojified = EmojiProvider.emojify(new String(v), view);
|
||||||
|
|
||||||
if (source instanceof Spanned && emojified != null) {
|
if (source instanceof Spanned && emojified != null) {
|
||||||
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);
|
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import android.util.AttributeSet;
|
|||||||
|
|
||||||
import androidx.appcompat.widget.AppCompatImageView;
|
import androidx.appcompat.widget.AppCompatImageView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
public class EmojiImageView extends AppCompatImageView {
|
public class EmojiImageView extends AppCompatImageView {
|
||||||
public EmojiImageView(Context context) {
|
public EmojiImageView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@@ -15,6 +17,10 @@ public class EmojiImageView extends AppCompatImageView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setImageEmoji(CharSequence emoji) {
|
public void setImageEmoji(CharSequence emoji) {
|
||||||
setImageDrawable(EmojiProvider.getInstance(getContext()).getEmojiDrawable(emoji));
|
if (isInEditMode()) {
|
||||||
|
setImageResource(R.drawable.ic_emoji);
|
||||||
|
} else {
|
||||||
|
setImageDrawable(EmojiProvider.getEmojiDrawable(getContext(), emoji));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.viewpager.widget.PagerAdapter;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.util.ResUtil;
|
import org.thoughtcrime.securesms.util.ResUtil;
|
||||||
@@ -66,7 +67,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
|||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
models.add(recentModel);
|
models.add(recentModel);
|
||||||
models.addAll(EmojiPages.DISPLAY_PAGES);
|
models.addAll(EmojiSource.getLatest().getDisplayPages());
|
||||||
|
|
||||||
currentPosition = recentModel.getEmoji().size() > 0 ? 0 : 1;
|
currentPosition = recentModel.getEmoji().size() > 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface EmojiPageModel {
|
public interface EmojiPageModel {
|
||||||
int getIconAttr();
|
int getIconAttr();
|
||||||
List<String> getEmoji();
|
List<String> getEmoji();
|
||||||
List<Emoji> getDisplayEmoji();
|
List<Emoji> getDisplayEmoji();
|
||||||
boolean hasSpriteMap();
|
@Nullable Uri getSpriteUri();
|
||||||
String getSprite();
|
|
||||||
boolean isDynamic();
|
boolean isDynamic();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
|||||||
layoutManager = new GridLayoutManager(context, 8);
|
layoutManager = new GridLayoutManager(context, 8);
|
||||||
scrollDisabler = new ScrollDisabler();
|
scrollDisabler = new ScrollDisabler();
|
||||||
popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
|
popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
|
||||||
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
|
adapter = new EmojiPageViewGridAdapter(popup,
|
||||||
popup,
|
|
||||||
emojiSelectionListener,
|
emojiSelectionListener,
|
||||||
this,
|
this,
|
||||||
allowVariations);
|
allowVariations);
|
||||||
|
|||||||
@@ -19,20 +19,17 @@ import java.util.List;
|
|||||||
public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageViewGridAdapter.EmojiViewHolder> implements PopupWindow.OnDismissListener {
|
public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageViewGridAdapter.EmojiViewHolder> implements PopupWindow.OnDismissListener {
|
||||||
|
|
||||||
private final List<Emoji> emojiList;
|
private final List<Emoji> emojiList;
|
||||||
private final EmojiProvider emojiProvider;
|
|
||||||
private final EmojiVariationSelectorPopup popup;
|
private final EmojiVariationSelectorPopup popup;
|
||||||
private final VariationSelectorListener variationSelectorListener;
|
private final VariationSelectorListener variationSelectorListener;
|
||||||
private final EmojiEventListener emojiEventListener;
|
private final EmojiEventListener emojiEventListener;
|
||||||
private final boolean allowVariations;
|
private final boolean allowVariations;
|
||||||
|
|
||||||
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
|
public EmojiPageViewGridAdapter(@NonNull EmojiVariationSelectorPopup popup,
|
||||||
@NonNull EmojiVariationSelectorPopup popup,
|
|
||||||
@NonNull EmojiEventListener emojiEventListener,
|
@NonNull EmojiEventListener emojiEventListener,
|
||||||
@NonNull VariationSelectorListener variationSelectorListener,
|
@NonNull VariationSelectorListener variationSelectorListener,
|
||||||
boolean allowVariations)
|
boolean allowVariations)
|
||||||
{
|
{
|
||||||
this.emojiList = new ArrayList<>();
|
this.emojiList = new ArrayList<>();
|
||||||
this.emojiProvider = emojiProvider;
|
|
||||||
this.popup = popup;
|
this.popup = popup;
|
||||||
this.emojiEventListener = emojiEventListener;
|
this.emojiEventListener = emojiEventListener;
|
||||||
this.variationSelectorListener = variationSelectorListener;
|
this.variationSelectorListener = variationSelectorListener;
|
||||||
@@ -51,7 +48,7 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
|
|||||||
public void onBindViewHolder(@NonNull EmojiViewHolder viewHolder, int i) {
|
public void onBindViewHolder(@NonNull EmojiViewHolder viewHolder, int i) {
|
||||||
Emoji emoji = emojiList.get(i);
|
Emoji emoji = emojiList.get(i);
|
||||||
|
|
||||||
Drawable drawable = emojiProvider.getEmojiDrawable(emoji.getValue());
|
final Drawable drawable = EmojiProvider.getEmojiDrawable(viewHolder.imageView.getContext(), emoji.getValue());
|
||||||
|
|
||||||
if (drawable != null) {
|
if (drawable != null) {
|
||||||
viewHolder.textView.setVisibility(View.GONE);
|
viewHolder.textView.setVisibility(View.GONE);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
@@ -9,8 +8,6 @@ import android.graphics.Paint;
|
|||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -20,81 +17,42 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
|
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
|
||||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiPageBitmap;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiTree;
|
import org.thoughtcrime.securesms.emoji.EmojiPageCache;
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||||
|
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||||
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
||||||
import org.whispersystems.libsignal.util.Pair;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
class EmojiProvider {
|
class EmojiProvider {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(EmojiProvider.class);
|
private static final String TAG = Log.tag(EmojiProvider.class);
|
||||||
private static volatile EmojiProvider instance = null;
|
private static final Paint PAINT = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
|
||||||
private static final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
|
|
||||||
|
|
||||||
private final EmojiTree emojiTree = new EmojiTree();
|
static @Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) {
|
||||||
|
|
||||||
private static final int EMOJI_RAW_HEIGHT = 64;
|
|
||||||
private static final int EMOJI_RAW_WIDTH = 64;
|
|
||||||
private static final int EMOJI_VERT_PAD = 0;
|
|
||||||
private static final int EMOJI_PER_ROW = 16;
|
|
||||||
|
|
||||||
private final float decodeScale;
|
|
||||||
private final float verticalPad;
|
|
||||||
|
|
||||||
public static EmojiProvider getInstance(Context context) {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (EmojiProvider.class) {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new EmojiProvider(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EmojiProvider(Context context) {
|
|
||||||
this.decodeScale = Math.min(1f, context.getResources().getDimension(R.dimen.emoji_drawer_size) / EMOJI_RAW_HEIGHT);
|
|
||||||
this.verticalPad = EMOJI_VERT_PAD * this.decodeScale;
|
|
||||||
|
|
||||||
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
|
|
||||||
if (page.hasSpriteMap()) {
|
|
||||||
EmojiPageBitmap pageBitmap = new EmojiPageBitmap(context, page, decodeScale);
|
|
||||||
|
|
||||||
List<String> emojis = page.getEmoji();
|
|
||||||
for (int i = 0; i < emojis.size(); i++) {
|
|
||||||
emojiTree.add(emojis.get(i), new EmojiDrawInfo(pageBitmap, i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Pair<String,String> obsolete : EmojiPages.OBSOLETE) {
|
|
||||||
emojiTree.add(obsolete.first(), emojiTree.getEmoji(obsolete.second(), 0, obsolete.second().length()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) {
|
|
||||||
if (text == null) return null;
|
if (text == null) return null;
|
||||||
return new EmojiParser(emojiTree).findCandidates(text);
|
return new EmojiParser(EmojiSource.getLatest().getEmojiTree()).findCandidates(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) {
|
static @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) {
|
||||||
return emojify(getCandidates(text), text, tv);
|
if (tv.isInEditMode()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return emojify(getCandidates(text), text, tv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable Spannable emojify(@Nullable EmojiParser.CandidateList matches,
|
static @Nullable Spannable emojify(@Nullable EmojiParser.CandidateList matches,
|
||||||
@Nullable CharSequence text,
|
@Nullable CharSequence text,
|
||||||
@NonNull TextView tv) {
|
@NonNull TextView tv)
|
||||||
if (matches == null || text == null) return null;
|
{
|
||||||
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
if (matches == null || text == null || tv.isInEditMode()) return null;
|
||||||
|
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||||
|
|
||||||
for (EmojiParser.Candidate candidate : matches) {
|
for (EmojiParser.Candidate candidate : matches) {
|
||||||
Drawable drawable = getEmojiDrawable(candidate.getDrawInfo());
|
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo());
|
||||||
|
|
||||||
if (drawable != null) {
|
if (drawable != null) {
|
||||||
builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(),
|
builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(),
|
||||||
@@ -105,49 +63,69 @@ class EmojiProvider {
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable Drawable getEmojiDrawable(CharSequence emoji) {
|
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji) {
|
||||||
EmojiDrawInfo drawInfo = emojiTree.getEmoji(emoji, 0, emoji.length());
|
EmojiDrawInfo drawInfo = EmojiSource.getLatest().getEmojiTree().getEmoji(emoji, 0, emoji.length());
|
||||||
return getEmojiDrawable(drawInfo);
|
return getEmojiDrawable(context, drawInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable Drawable getEmojiDrawable(@Nullable EmojiDrawInfo drawInfo) {
|
private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo) {
|
||||||
if (drawInfo == null) {
|
if (drawInfo == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale);
|
final int lowMemoryDecodeScale = DeviceProperties.isLowMemoryDevice(context) ? 2 : 1;
|
||||||
drawInfo.getPage().get().addListener(new FutureTaskListener<Bitmap>() {
|
final EmojiSource source = EmojiSource.getLatest();
|
||||||
@Override public void onSuccess(final Bitmap result) {
|
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
|
||||||
ThreadUtil.runOnMain(() -> drawable.setBitmap(result));
|
|
||||||
}
|
EmojiPageCache.INSTANCE
|
||||||
|
.load(context, drawInfo.getPage(), lowMemoryDecodeScale)
|
||||||
|
.addListener(new FutureTaskListener<Bitmap>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Bitmap result) {
|
||||||
|
ThreadUtil.runOnMain(() -> drawable.setBitmap(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(ExecutionException exception) {
|
||||||
|
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@Override public void onFailure(ExecutionException error) {
|
|
||||||
Log.w(TAG, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return drawable;
|
return drawable;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmojiDrawable extends Drawable {
|
static final class EmojiDrawable extends Drawable {
|
||||||
private final EmojiDrawInfo info;
|
private final float intrinsicWidth;
|
||||||
private Bitmap bmp;
|
private final float intrinsicHeight;
|
||||||
private float intrinsicWidth;
|
private final Rect emojiBounds;
|
||||||
private float intrinsicHeight;
|
|
||||||
|
private Bitmap bmp;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getIntrinsicWidth() {
|
public int getIntrinsicWidth() {
|
||||||
return (int)intrinsicWidth;
|
return (int) intrinsicWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getIntrinsicHeight() {
|
public int getIntrinsicHeight() {
|
||||||
return (int)intrinsicHeight;
|
return (int) intrinsicHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiDrawable(EmojiDrawInfo info, float decodeScale) {
|
EmojiDrawable(@NonNull EmojiSource source, @NonNull EmojiDrawInfo info, int lowMemoryDecodeScale) {
|
||||||
this.info = info;
|
this.intrinsicWidth = (source.getMetrics().getRawWidth() * source.getDecodeScale()) / lowMemoryDecodeScale;
|
||||||
this.intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale;
|
this.intrinsicHeight = (source.getMetrics().getRawHeight() * source.getDecodeScale()) / lowMemoryDecodeScale;
|
||||||
this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale;
|
|
||||||
|
final int glyphWidth = (int) (intrinsicWidth);
|
||||||
|
final int glyphHeight = (int) (intrinsicHeight);
|
||||||
|
final int index = info.getIndex();
|
||||||
|
final int emojiPerRow = source.getMetrics().getPerRow();
|
||||||
|
final int xStart = (index % emojiPerRow) * glyphWidth;
|
||||||
|
final int yStart = (index / emojiPerRow) * glyphHeight;
|
||||||
|
|
||||||
|
this.emojiBounds = new Rect(xStart,
|
||||||
|
yStart,
|
||||||
|
xStart + glyphWidth,
|
||||||
|
yStart + glyphHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -156,22 +134,15 @@ class EmojiProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int row = info.getIndex() / EMOJI_PER_ROW;
|
|
||||||
final int row_index = info.getIndex() % EMOJI_PER_ROW;
|
|
||||||
|
|
||||||
canvas.drawBitmap(bmp,
|
canvas.drawBitmap(bmp,
|
||||||
new Rect((int)(row_index * intrinsicWidth),
|
emojiBounds,
|
||||||
(int)(row * intrinsicHeight + row * verticalPad)+1,
|
|
||||||
(int)(((row_index + 1) * intrinsicWidth)-1),
|
|
||||||
(int)((row + 1) * intrinsicHeight + row * verticalPad)-1),
|
|
||||||
getBounds(),
|
getBounds(),
|
||||||
paint);
|
PAINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
|
|
||||||
public void setBitmap(Bitmap bitmap) {
|
public void setBitmap(Bitmap bitmap) {
|
||||||
ThreadUtil.assertMainThread();
|
ThreadUtil.assertMainThread();
|
||||||
if (VERSION.SDK_INT < VERSION_CODES.HONEYCOMB_MR1 || bmp == null || !bmp.sameAs(bitmap)) {
|
if (bmp == null || !bmp.sameAs(bitmap)) {
|
||||||
bmp = bitmap;
|
bmp = bitmap;
|
||||||
invalidateSelf();
|
invalidateSelf();
|
||||||
}
|
}
|
||||||
@@ -188,5 +159,4 @@ class EmojiProvider {
|
|||||||
@Override
|
@Override
|
||||||
public void setColorFilter(ColorFilter cf) { }
|
public void setColorFilter(ColorFilter cf) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ import androidx.core.content.ContextCompat;
|
|||||||
import androidx.core.widget.TextViewCompat;
|
import androidx.core.widget.TextViewCompat;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
@@ -91,9 +90,9 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
@Override
|
||||||
EmojiProvider provider = EmojiProvider.getInstance(getContext());
|
public void setText(@Nullable CharSequence text, BufferType type) {
|
||||||
EmojiParser.CandidateList candidates = provider.getCandidates(text);
|
EmojiParser.CandidateList candidates = isInEditMode() ? null : EmojiProvider.getCandidates(text);
|
||||||
|
|
||||||
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
||||||
int emojis = candidates.size();
|
int emojis = candidates.size();
|
||||||
@@ -119,23 +118,19 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
useSystemEmoji = useSystemEmoji();
|
useSystemEmoji = useSystemEmoji();
|
||||||
|
|
||||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||||
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")).append(Optional.fromNullable(overflowText).or("")), BufferType.NORMAL);
|
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.NORMAL);
|
||||||
|
|
||||||
if (getEllipsize() == TextUtils.TruncateAt.END && maxLength > 0) {
|
|
||||||
ellipsizeAnyTextForMaxLength();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
CharSequence emojified = provider.emojify(candidates, text, this);
|
CharSequence emojified = EmojiProvider.emojify(candidates, text, this);
|
||||||
super.setText(new SpannableStringBuilder(emojified).append(Optional.fromNullable(overflowText).or("")), BufferType.SPANNABLE);
|
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
|
||||||
|
}
|
||||||
|
|
||||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||||
if (getEllipsize() == TextUtils.TruncateAt.END) {
|
if (getText() != null && getText().length() > 0 && getEllipsize() == TextUtils.TruncateAt.END) {
|
||||||
if (maxLength > 0) {
|
if (maxLength > 0) {
|
||||||
ellipsizeAnyTextForMaxLength();
|
ellipsizeAnyTextForMaxLength();
|
||||||
} else {
|
} else if (getMaxLines() > 0) {
|
||||||
ellipsizeEmojiTextForMaxLines();
|
ellipsizeEmojiTextForMaxLines();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,12 +161,12 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
.append(ELLIPSIS)
|
.append(ELLIPSIS)
|
||||||
.append(Util.emptyIfNull(overflowText));
|
.append(Util.emptyIfNull(overflowText));
|
||||||
|
|
||||||
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
|
EmojiParser.CandidateList newCandidates = isInEditMode() ? null : EmojiProvider.getCandidates(newContent);
|
||||||
|
|
||||||
if (useSystemEmoji || newCandidates == null || newCandidates.size() == 0) {
|
if (useSystemEmoji || newCandidates == null || newCandidates.size() == 0) {
|
||||||
super.setText(newContent, BufferType.NORMAL);
|
super.setText(newContent, BufferType.NORMAL);
|
||||||
} else {
|
} else {
|
||||||
CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this);
|
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this);
|
||||||
super.setText(emojified, BufferType.SPANNABLE);
|
super.setText(emojified, BufferType.SPANNABLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,15 +188,16 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
if (lineCount > maxLines) {
|
if (lineCount > maxLines) {
|
||||||
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
||||||
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
|
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
|
||||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth(), TextUtils.TruncateAt.END);
|
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
|
||||||
|
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
|
||||||
|
|
||||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||||
newContent.append(getText().subSequence(0, overflowStart))
|
newContent.append(getText().subSequence(0, overflowStart))
|
||||||
.append(ellipsized.subSequence(0, ellipsized.length()))
|
.append(ellipsized.subSequence(0, ellipsized.length()))
|
||||||
.append(Optional.fromNullable(overflowText).or(""));
|
.append(Optional.fromNullable(overflowText).or(""));
|
||||||
|
|
||||||
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
|
EmojiParser.CandidateList newCandidates = isInEditMode() ? null : EmojiProvider.getCandidates(newContent);
|
||||||
CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this);
|
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this);
|
||||||
|
|
||||||
super.setText(emojified, BufferType.SPANNABLE);
|
super.setText(emojified, BufferType.SPANNABLE);
|
||||||
}
|
}
|
||||||
@@ -217,7 +213,7 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean useSystemEmoji() {
|
private boolean useSystemEmoji() {
|
||||||
return !forceCustom && TextSecurePreferences.isSystemEmojiPreferred(getContext());
|
return !forceCustom && SignalStore.settings().isPreferSystemEmoji();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -233,8 +229,8 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidateDrawable(@NonNull Drawable drawable) {
|
public void invalidateDrawable(@NonNull Drawable drawable) {
|
||||||
if (drawable instanceof EmojiDrawable) invalidate();
|
if (drawable instanceof EmojiProvider.EmojiDrawable) invalidate();
|
||||||
else super.invalidateDrawable(drawable);
|
else super.invalidateDrawable(drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -7,50 +7,20 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||||
|
import org.thoughtcrime.securesms.emoji.ObsoleteEmoji;
|
||||||
import org.thoughtcrime.securesms.util.StringUtil;
|
import org.thoughtcrime.securesms.util.StringUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.libsignal.util.Pair;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public final class EmojiUtil {
|
public final class EmojiUtil {
|
||||||
|
|
||||||
private static final Map<String, String> VARIATION_MAP = new HashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
|
|
||||||
for (Emoji emoji : page.getDisplayEmoji()) {
|
|
||||||
for (String variation : emoji.getVariations()) {
|
|
||||||
VARIATION_MAP.put(variation, emoji.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final int MAX_EMOJI_LENGTH;
|
|
||||||
static {
|
|
||||||
int max = 0;
|
|
||||||
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
|
|
||||||
for (String emoji : page.getEmoji()) {
|
|
||||||
max = Math.max(max, emoji.length());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MAX_EMOJI_LENGTH = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]+)+)+$");
|
private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]+)+)+$");
|
||||||
|
|
||||||
private EmojiUtil() {}
|
private EmojiUtil() {}
|
||||||
|
|
||||||
public static List<EmojiPageModel> getDisplayPages() {
|
|
||||||
return EmojiPages.DISPLAY_PAGES;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will return all ways we know of expressing a singular emoji. This is to aid in search,
|
* This will return all ways we know of expressing a singular emoji. This is to aid in search,
|
||||||
* where some platforms may send an emoji we've locally marked as 'obsolete'.
|
* where some platforms may send an emoji we've locally marked as 'obsolete'.
|
||||||
@@ -60,11 +30,11 @@ public final class EmojiUtil {
|
|||||||
|
|
||||||
out.add(emoji);
|
out.add(emoji);
|
||||||
|
|
||||||
for (Pair<String, String> pair : EmojiPages.OBSOLETE) {
|
for (ObsoleteEmoji obsoleteEmoji : EmojiSource.getLatest().getObsolete()) {
|
||||||
if (pair.first().equals(emoji)) {
|
if (obsoleteEmoji.getObsolete().equals(emoji)) {
|
||||||
out.add(pair.second());
|
out.add(obsoleteEmoji.getReplaceWith());
|
||||||
} else if (pair.second().equals(emoji)) {
|
} else if (obsoleteEmoji.getReplaceWith().equals(emoji)) {
|
||||||
out.add(pair.first());
|
out.add(obsoleteEmoji.getObsolete());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +49,7 @@ public final class EmojiUtil {
|
|||||||
* If the emoji has no skin variations, this function will return the original emoji.
|
* If the emoji has no skin variations, this function will return the original emoji.
|
||||||
*/
|
*/
|
||||||
public static @NonNull String getCanonicalRepresentation(@NonNull String emoji) {
|
public static @NonNull String getCanonicalRepresentation(@NonNull String emoji) {
|
||||||
String canonical = VARIATION_MAP.get(emoji);
|
String canonical = EmojiSource.getLatest().getVariationMap().get(emoji);
|
||||||
return canonical != null ? canonical : emoji;
|
return canonical != null ? canonical : emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +60,7 @@ public final class EmojiUtil {
|
|||||||
if (Util.isEmpty(emoji)) {
|
if (Util.isEmpty(emoji)) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return EmojiProvider.getInstance(context).getEmojiDrawable(emoji);
|
return EmojiProvider.getEmojiDrawable(context, emoji);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +71,7 @@ public final class EmojiUtil {
|
|||||||
* followed by a more wide check for all of the valid emoji unicode ranges (which could lead to
|
* followed by a more wide check for all of the valid emoji unicode ranges (which could lead to
|
||||||
* some false positives). YMMV.
|
* some false positives). YMMV.
|
||||||
*/
|
*/
|
||||||
public static boolean isEmoji(@NonNull Context context, @Nullable String text) {
|
public static boolean isEmoji(@Nullable String text) {
|
||||||
if (Util.isEmpty(text)) {
|
if (Util.isEmpty(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -110,7 +80,7 @@ public final class EmojiUtil {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiParser.CandidateList candidates = EmojiProvider.getInstance(context).getCandidates(text);
|
EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text);
|
||||||
|
|
||||||
return (candidates != null && candidates.size() > 0) || EMOJI_PATTERN.matcher(text).matches();
|
return (candidates != null && candidates.size() > 0) || EMOJI_PATTERN.matcher(text).matches();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow {
|
|||||||
|
|
||||||
for (String variation : variations) {
|
for (String variation : variations) {
|
||||||
ImageView imageView = (ImageView) LayoutInflater.from(context).inflate(R.layout.emoji_variation_selector_item, list, false);
|
ImageView imageView = (ImageView) LayoutInflater.from(context).inflate(R.layout.emoji_variation_selector_item, list, false);
|
||||||
imageView.setImageDrawable(EmojiProvider.getInstance(context).getEmojiDrawable(variation));
|
imageView.setImageDrawable(EmojiProvider.getEmojiDrawable(context, variation));
|
||||||
imageView.setOnClickListener(v -> {
|
imageView.setOnClickListener(v -> {
|
||||||
listener.onEmojiSelected(variation);
|
listener.onEmojiSelected(variation);
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.components.emoji;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.fasterxml.jackson.databind.type.CollectionType;
|
import com.fasterxml.jackson.databind.type.CollectionType;
|
||||||
@@ -63,11 +65,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
|||||||
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public boolean hasSpriteMap() {
|
@Override public @Nullable Uri getSpriteUri() {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public String getSprite() {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.AttrRes;
|
import androidx.annotation.AttrRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class StaticEmojiPageModel implements EmojiPageModel {
|
public class StaticEmojiPageModel implements EmojiPageModel {
|
||||||
@AttrRes private final int iconAttr;
|
@AttrRes private final int iconAttr;
|
||||||
@NonNull private final List<Emoji> emoji;
|
@NonNull private final List<Emoji> emoji;
|
||||||
@Nullable private final String sprite;
|
@Nullable private final Uri sprite;
|
||||||
|
|
||||||
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull String[] strings, @Nullable String sprite) {
|
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull String[] strings, @Nullable Uri sprite) {
|
||||||
List<Emoji> emoji = new ArrayList<>(strings.length);
|
List<Emoji> emoji = new ArrayList<>(strings.length);
|
||||||
for (String s : strings) {
|
for (String s : strings) {
|
||||||
emoji.add(new Emoji(s));
|
emoji.add(new Emoji(Collections.singletonList(s)));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.iconAttr = iconAttr;
|
this.iconAttr = iconAttr;
|
||||||
@@ -25,9 +27,9 @@ public class StaticEmojiPageModel implements EmojiPageModel {
|
|||||||
this.sprite = sprite;
|
this.sprite = sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull Emoji[] emoji, @Nullable String sprite) {
|
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull List<Emoji> emoji, @Nullable Uri sprite) {
|
||||||
this.iconAttr = iconAttr;
|
this.iconAttr = iconAttr;
|
||||||
this.emoji = Arrays.asList(emoji);
|
this.emoji = Collections.unmodifiableList(emoji);
|
||||||
this.sprite = sprite;
|
this.sprite = sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,12 +52,7 @@ public class StaticEmojiPageModel implements EmojiPageModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasSpriteMap() {
|
public @Nullable Uri getSpriteUri() {
|
||||||
return sprite != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable String getSprite() {
|
|
||||||
return sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ package org.thoughtcrime.securesms.components.emoji.parsing;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiPage;
|
||||||
|
|
||||||
public class EmojiDrawInfo {
|
public class EmojiDrawInfo {
|
||||||
|
|
||||||
private final EmojiPageBitmap page;
|
private final EmojiPage page;
|
||||||
private final int index;
|
private final int index;
|
||||||
|
|
||||||
public EmojiDrawInfo(final @NonNull EmojiPageBitmap page, final int index) {
|
public EmojiDrawInfo(final @NonNull EmojiPage page, final int index) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.index = index;
|
this.index = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull EmojiPageBitmap getPage() {
|
public @NonNull EmojiPage getPage() {
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
|
||||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
|
|
||||||
public class EmojiPageBitmap {
|
|
||||||
|
|
||||||
private static final String TAG = Log.tag(EmojiPageBitmap.class);
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final EmojiPageModel model;
|
|
||||||
private final float decodeScale;
|
|
||||||
|
|
||||||
private SoftReference<Bitmap> bitmapReference;
|
|
||||||
private ListenableFutureTask<Bitmap> task;
|
|
||||||
|
|
||||||
public EmojiPageBitmap(@NonNull Context context, @NonNull EmojiPageModel model, float decodeScale) {
|
|
||||||
this.context = context.getApplicationContext();
|
|
||||||
this.model = model;
|
|
||||||
this.decodeScale = decodeScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
public ListenableFutureTask<Bitmap> get() {
|
|
||||||
ThreadUtil.assertMainThread();
|
|
||||||
|
|
||||||
if (bitmapReference != null && bitmapReference.get() != null) {
|
|
||||||
return new ListenableFutureTask<>(bitmapReference.get());
|
|
||||||
} else if (task != null) {
|
|
||||||
return task;
|
|
||||||
} else {
|
|
||||||
Callable<Bitmap> callable = () -> {
|
|
||||||
try {
|
|
||||||
Log.i(TAG, "loading page " + model.getSprite());
|
|
||||||
return loadPage();
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.w(TAG, ioe);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
task = new ListenableFutureTask<>(callable);
|
|
||||||
SimpleTask.run(() -> {
|
|
||||||
task.run();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
unused -> task = null);
|
|
||||||
}
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap loadPage() throws IOException {
|
|
||||||
if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get();
|
|
||||||
|
|
||||||
|
|
||||||
float scale = decodeScale;
|
|
||||||
AssetManager assetManager = context.getAssets();
|
|
||||||
InputStream assetStream = assetManager.open(model.getSprite());
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
|
|
||||||
if (Util.isLowMemory(context)) {
|
|
||||||
Log.i(TAG, "Low memory detected. Changing sample size.");
|
|
||||||
options.inSampleSize = 2;
|
|
||||||
scale = decodeScale * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
Stopwatch stopwatch = new Stopwatch(model.getSprite());
|
|
||||||
Bitmap bitmap = BitmapFactory.decodeStream(assetStream, null, options);
|
|
||||||
stopwatch.split("decode");
|
|
||||||
|
|
||||||
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, (int)(bitmap.getWidth() * scale), (int)(bitmap.getHeight() * scale), true);
|
|
||||||
stopwatch.split("scale");
|
|
||||||
stopwatch.stop(TAG);
|
|
||||||
|
|
||||||
bitmapReference = new SoftReference<>(scaledBitmap);
|
|
||||||
Log.i(TAG, "onPageLoaded(" + model.getSprite() + ") originalByteCount: " + bitmap.getByteCount()
|
|
||||||
+ " scaledByteCount: " + scaledBitmap.getByteCount()
|
|
||||||
+ " scaledSize: " + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight());
|
|
||||||
return scaledBitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull String toString() {
|
|
||||||
return model.getSprite();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -87,7 +87,7 @@ public final class ReminderView extends FrameLayout {
|
|||||||
break;
|
break;
|
||||||
case ERROR:
|
case ERROR:
|
||||||
container.setBackgroundResource(R.drawable.reminder_background_error);
|
container.setBackgroundResource(R.drawable.reminder_background_error);
|
||||||
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
|
text.setTextColor(ContextCompat.getColor(getContext(), R.color.core_black));
|
||||||
break;
|
break;
|
||||||
case TERMINAL:
|
case TERMINAL:
|
||||||
container.setBackgroundResource(R.drawable.reminder_background_terminal);
|
container.setBackgroundResource(R.drawable.reminder_background_terminal);
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||||
|
|
||||||
|
open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||||
|
|
||||||
|
private val dynamicTheme = DynamicNoActionBarTheme()
|
||||||
|
|
||||||
|
protected lateinit var navController: NavController
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
|
setContentView(R.layout.dsl_settings_activity)
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
val navGraphId = intent.getIntExtra(ARG_NAV_GRAPH, -1)
|
||||||
|
if (navGraphId == -1) {
|
||||||
|
throw IllegalStateException("No navgraph id was passed to activity")
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragment: NavHostFragment = NavHostFragment.create(navGraphId, intent.getBundleExtra(ARG_START_BUNDLE))
|
||||||
|
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.nav_host_fragment, fragment)
|
||||||
|
.commitNow()
|
||||||
|
|
||||||
|
navController = fragment.navController
|
||||||
|
} else {
|
||||||
|
val fragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||||
|
navController = fragment.navController
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicTheme.onCreate(this)
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, OnBackPressed())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
dynamicTheme.onResume(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNavigateUp(): Boolean {
|
||||||
|
return if (!Navigation.findNavController(this, R.id.nav_host_fragment).popBackStack()) {
|
||||||
|
onWillFinish()
|
||||||
|
finish()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onWillFinish() {}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ARG_NAV_GRAPH = "nav_graph"
|
||||||
|
const val ARG_START_BUNDLE = "start_bundle"
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class OnBackPressed : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
onNavigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings
|
||||||
|
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.RadioButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
|
||||||
|
class DSLSettingsAdapter : MappingAdapter() {
|
||||||
|
init {
|
||||||
|
registerFactory(ClickPreference::class.java, LayoutFactory(::ClickPreferenceViewHolder, 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))
|
||||||
|
registerFactory(ExternalLinkPreference::class.java, LayoutFactory(::ExternalLinkPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||||
|
registerFactory(DividerPreference::class.java, LayoutFactory(::DividerPreferenceViewHolder, R.layout.dsl_divider_item))
|
||||||
|
registerFactory(SectionHeaderPreference::class.java, LayoutFactory(::SectionHeaderPreferenceViewHolder, R.layout.dsl_section_header))
|
||||||
|
registerFactory(SwitchPreference::class.java, LayoutFactory(::SwitchPreferenceViewHolder, R.layout.dsl_switch_preference_item))
|
||||||
|
registerFactory(RadioPreference::class.java, LayoutFactory(::RadioPreferenceViewHolder, R.layout.dsl_radio_preference_item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PreferenceViewHolder<T : PreferenceModel<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
|
||||||
|
protected val iconView: ImageView = itemView.findViewById(R.id.icon)
|
||||||
|
protected val titleView: TextView = itemView.findViewById(R.id.title)
|
||||||
|
protected val summaryView: TextView = itemView.findViewById(R.id.summary)
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun bind(model: T) {
|
||||||
|
listOf(itemView, titleView, summaryView).forEach {
|
||||||
|
it.isEnabled = model.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.iconId != -1) {
|
||||||
|
iconView.setImageResource(model.iconId)
|
||||||
|
iconView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
iconView.setImageDrawable(null)
|
||||||
|
iconView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val title = model.title?.resolve(context)
|
||||||
|
if (title != null) {
|
||||||
|
titleView.text = model.title?.resolve(context)
|
||||||
|
titleView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
titleView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val summary = model.summary?.resolve(context)
|
||||||
|
if (summary != null) {
|
||||||
|
summaryView.text = summary
|
||||||
|
summaryView.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val spans = (summaryView.text as? Spanned)?.getSpans(0, summaryView.text.length, ClickableSpan::class.java)
|
||||||
|
if (spans?.isEmpty() == false) {
|
||||||
|
summaryView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
} else {
|
||||||
|
summaryView.movementMethod = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
summaryView.visibility = View.GONE
|
||||||
|
summaryView.movementMethod = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextPreferenceViewHolder(itemView: View) : PreferenceViewHolder<TextPreference>(itemView)
|
||||||
|
|
||||||
|
class ClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ClickPreference>(itemView) {
|
||||||
|
override fun bind(model: ClickPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
itemView.setOnClickListener { model.onClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<RadioListPreference>(itemView) {
|
||||||
|
override fun bind(model: RadioListPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
|
||||||
|
summaryView.visibility = View.VISIBLE
|
||||||
|
summaryView.text = model.listItems[model.selected]
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(model.title.resolve(context))
|
||||||
|
.setSingleChoiceItems(model.listItems, model.selected) { dialog, which ->
|
||||||
|
model.onSelected(which)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiSelectListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<MultiSelectListPreference>(itemView) {
|
||||||
|
override fun bind(model: MultiSelectListPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
|
||||||
|
summaryView.visibility = View.VISIBLE
|
||||||
|
val summaryText = model.selected
|
||||||
|
.mapIndexed { index, isChecked -> if (isChecked) model.listItems[index] else null }
|
||||||
|
.filterNotNull()
|
||||||
|
.joinToString(", ")
|
||||||
|
|
||||||
|
if (summaryText.isEmpty()) {
|
||||||
|
summaryView.setText(R.string.preferences__none)
|
||||||
|
} else {
|
||||||
|
summaryView.text = summaryText
|
||||||
|
}
|
||||||
|
|
||||||
|
val selected = model.selected.copyOf()
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(model.title.resolve(context))
|
||||||
|
.setMultiChoiceItems(model.listItems, selected) { _, _, _ ->
|
||||||
|
// Intentionally empty
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||||
|
.setPositiveButton(android.R.string.ok) { d, _ ->
|
||||||
|
model.onSelected(selected)
|
||||||
|
d.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SwitchPreferenceViewHolder(itemView: View) : PreferenceViewHolder<SwitchPreference>(itemView) {
|
||||||
|
|
||||||
|
private val switchWidget: SwitchMaterial = itemView.findViewById(R.id.switch_widget)
|
||||||
|
|
||||||
|
override fun bind(model: SwitchPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
switchWidget.isEnabled = model.isEnabled
|
||||||
|
switchWidget.isChecked = model.isChecked
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
model.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RadioPreferenceViewHolder(itemView: View) : PreferenceViewHolder<RadioPreference>(itemView) {
|
||||||
|
|
||||||
|
private val radioButton: RadioButton = itemView.findViewById(R.id.radio_widget)
|
||||||
|
|
||||||
|
override fun bind(model: RadioPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
radioButton.isChecked = model.isChecked
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
model.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExternalLinkPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ExternalLinkPreference>(itemView) {
|
||||||
|
override fun bind(model: ExternalLinkPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
|
||||||
|
val externalLinkIcon = requireNotNull(ContextCompat.getDrawable(context, R.drawable.ic_open_20))
|
||||||
|
externalLinkIcon.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20))
|
||||||
|
|
||||||
|
if (ViewUtil.isLtr(itemView)) {
|
||||||
|
titleView.setCompoundDrawables(null, null, externalLinkIcon, null)
|
||||||
|
} else {
|
||||||
|
titleView.setCompoundDrawables(externalLinkIcon, null, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.setOnClickListener { CommunicationActions.openBrowserLink(itemView.context, itemView.context.getString(model.linkId)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DividerPreferenceViewHolder(itemView: View) : MappingViewHolder<DividerPreference>(itemView) {
|
||||||
|
override fun bind(model: DividerPreference) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
class SectionHeaderPreferenceViewHolder(itemView: View) : MappingViewHolder<SectionHeaderPreference>(itemView) {
|
||||||
|
|
||||||
|
private val sectionHeader: TextView = itemView.findViewById(R.id.section_header)
|
||||||
|
|
||||||
|
override fun bind(model: SectionHeaderPreference) {
|
||||||
|
sectionHeader.text = model.title.resolve(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EdgeEffect
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.annotation.MenuRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
|
||||||
|
abstract class DSLSettingsFragment(
|
||||||
|
@StringRes private val titleId: Int,
|
||||||
|
@MenuRes private val menuId: Int = -1,
|
||||||
|
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
|
||||||
|
) : Fragment(layoutId) {
|
||||||
|
|
||||||
|
private lateinit var recyclerView: RecyclerView
|
||||||
|
private lateinit var toolbarShadowHelper: ToolbarShadowHelper
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||||
|
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
|
||||||
|
|
||||||
|
toolbar.setTitle(titleId)
|
||||||
|
|
||||||
|
toolbar.setNavigationOnClickListener {
|
||||||
|
requireActivity().onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuId != -1) {
|
||||||
|
toolbar.inflateMenu(menuId)
|
||||||
|
toolbar.setOnMenuItemClickListener { onOptionsItemSelected(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
recyclerView = view.findViewById(R.id.recycler)
|
||||||
|
recyclerView.edgeEffectFactory = EdgeEffectFactory()
|
||||||
|
toolbarShadowHelper = ToolbarShadowHelper(toolbarShadow)
|
||||||
|
val adapter = DSLSettingsAdapter()
|
||||||
|
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
recyclerView.addOnScrollListener(toolbarShadowHelper)
|
||||||
|
|
||||||
|
bindAdapter(adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
toolbarShadowHelper.onScrolled(recyclerView, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun bindAdapter(adapter: DSLSettingsAdapter)
|
||||||
|
|
||||||
|
private class EdgeEffectFactory : RecyclerView.EdgeEffectFactory() {
|
||||||
|
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
|
||||||
|
return super.createEdgeEffect(view, direction).apply {
|
||||||
|
if (Build.VERSION.SDK_INT > 21) {
|
||||||
|
color =
|
||||||
|
requireNotNull(ContextCompat.getColor(view.context, R.color.settings_ripple_color))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarShadowHelper(private val toolbarShadow: View) : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
private var lastAnimationState = ToolbarAnimationState.NONE
|
||||||
|
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
val newAnimationState =
|
||||||
|
if (recyclerView.canScrollVertically(-1)) ToolbarAnimationState.SHOW else ToolbarAnimationState.HIDE
|
||||||
|
|
||||||
|
if (newAnimationState == lastAnimationState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (newAnimationState) {
|
||||||
|
ToolbarAnimationState.NONE -> throw AssertionError()
|
||||||
|
ToolbarAnimationState.HIDE -> toolbarShadow.animate().alpha(0f)
|
||||||
|
ToolbarAnimationState.SHOW -> toolbarShadow.animate().alpha(1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAnimationState = newAnimationState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class ToolbarAnimationState {
|
||||||
|
NONE,
|
||||||
|
HIDE,
|
||||||
|
SHOW
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
|
|
||||||
|
sealed class DSLSettingsText {
|
||||||
|
|
||||||
|
private data class FromResource(
|
||||||
|
@StringRes private val stringId: Int,
|
||||||
|
@ColorInt private val textColor: Int?
|
||||||
|
) : DSLSettingsText() {
|
||||||
|
override fun resolve(context: Context): CharSequence {
|
||||||
|
val text = context.getString(stringId)
|
||||||
|
|
||||||
|
return if (textColor == null) {
|
||||||
|
text
|
||||||
|
} else {
|
||||||
|
SpanUtil.color(textColor, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class FromCharSequence(private val charSequence: CharSequence) : DSLSettingsText() {
|
||||||
|
override fun resolve(context: Context): CharSequence = charSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun resolve(context: Context): CharSequence
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(@StringRes stringId: Int, @ColorInt textColor: Int? = null): DSLSettingsText =
|
||||||
|
FromResource(stringId, textColor)
|
||||||
|
|
||||||
|
fun from(charSequence: CharSequence): DSLSettingsText = FromCharSequence(charSequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.navigation.NavDirections
|
||||||
|
import org.thoughtcrime.securesms.MainActivity
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||||
|
import org.thoughtcrime.securesms.help.HelpFragment
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||||
|
import org.thoughtcrime.securesms.util.CachedInflater
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||||
|
|
||||||
|
private const val START_LOCATION = "app.settings.start.location"
|
||||||
|
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
|
||||||
|
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
|
||||||
|
|
||||||
|
class AppSettingsActivity : DSLSettingsActivity() {
|
||||||
|
|
||||||
|
private var wasConfigurationUpdated = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
|
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
|
||||||
|
intent?.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState, ready)
|
||||||
|
|
||||||
|
val startingAction: NavDirections? = if (intent?.categories?.contains(NOTIFICATION_CATEGORY) == true) {
|
||||||
|
AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||||
|
} else {
|
||||||
|
when (StartLocation.fromCode(intent?.getIntExtra(START_LOCATION, StartLocation.HOME.code))) {
|
||||||
|
StartLocation.HOME -> null
|
||||||
|
StartLocation.BACKUPS -> AppSettingsFragmentDirections.actionDirectToBackupsPreferenceFragment()
|
||||||
|
StartLocation.HELP -> AppSettingsFragmentDirections.actionDirectToHelpFragment()
|
||||||
|
.setStartCategoryIndex(intent.getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0))
|
||||||
|
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
|
||||||
|
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startingAction == null && savedInstanceState != null) {
|
||||||
|
wasConfigurationUpdated = savedInstanceState.getBoolean(STATE_WAS_CONFIGURATION_UPDATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
startingAction?.let {
|
||||||
|
navController.navigate(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalStore.settings().onConfigurationSettingChanged.observe(this) { key ->
|
||||||
|
if (key == SettingsValues.THEME) {
|
||||||
|
DynamicTheme.setDefaultDayNightMode(this)
|
||||||
|
recreate()
|
||||||
|
} else if (key == SettingsValues.LANGUAGE) {
|
||||||
|
CachedInflater.from(this).clear()
|
||||||
|
wasConfigurationUpdated = true
|
||||||
|
recreate()
|
||||||
|
val intent = Intent(this, KeyCachingService::class.java)
|
||||||
|
intent.action = KeyCachingService.LOCALE_CHANGE_EVENT
|
||||||
|
startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putBoolean(STATE_WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWillFinish() {
|
||||||
|
if (wasConfigurationUpdated) {
|
||||||
|
setResult(MainActivity.RESULT_CONFIG_CHANGED)
|
||||||
|
} else {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun home(context: Context): Intent = getIntentForStartLocation(context, StartLocation.HOME)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun backups(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun help(context: Context, startCategoryIndex: Int = 0): Intent {
|
||||||
|
return getIntentForStartLocation(context, StartLocation.HOME)
|
||||||
|
.putExtra(HelpFragment.START_CATEGORY_INDEX, startCategoryIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun proxy(context: Context): Intent = getIntentForStartLocation(context, StartLocation.PROXY)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun notifications(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATIONS)
|
||||||
|
|
||||||
|
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||||
|
return Intent(context, AppSettingsActivity::class.java)
|
||||||
|
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||||
|
.putExtra(START_LOCATION, startLocation.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class StartLocation(val code: Int) {
|
||||||
|
HOME(0),
|
||||||
|
BACKUPS(1),
|
||||||
|
HELP(2),
|
||||||
|
PROXY(3),
|
||||||
|
NOTIFICATIONS(4);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromCode(code: Int?): StartLocation {
|
||||||
|
return values().find { code == it.code } ?: HOME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
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.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__menu_settings) {
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
adapter.registerFactory(BioPreference::class.java, MappingAdapter.LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
|
||||||
|
adapter.registerFactory(PaymentsPreference::class.java, MappingAdapter.LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
|
||||||
|
|
||||||
|
val viewModel = ViewModelProviders.of(this)[AppSettingsViewModel::class.java]
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
|
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: AppSettingsState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
|
||||||
|
customPref(
|
||||||
|
BioPreference(state.self) {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.AccountSettingsFragment__account),
|
||||||
|
iconId = R.drawable.ic_profile_circle_24,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__linked_devices),
|
||||||
|
iconId = R.drawable.ic_linked_devices_24,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_deviceActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (SignalStore.paymentsValues().paymentsAvailability.showPaymentsMenu()) {
|
||||||
|
customPref(
|
||||||
|
PaymentsPreference(
|
||||||
|
unreadCount = state.unreadPaymentsCount
|
||||||
|
) {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_paymentsActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__appearance),
|
||||||
|
iconId = R.drawable.ic_appearance_24,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_chats__chats),
|
||||||
|
iconId = R.drawable.ic_message_tinted_bitmap_24,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__notifications),
|
||||||
|
iconId = R.drawable.ic_bell_24,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__privacy),
|
||||||
|
iconId = R.drawable.ic_lock_24,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__data_and_storage),
|
||||||
|
iconId = R.drawable.ic_archive_24dp,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__help),
|
||||||
|
iconId = R.drawable.ic_help_24,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.AppSettingsFragment__invite_your_friends),
|
||||||
|
iconId = R.drawable.ic_invite_24,
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_inviteActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
externalLinkPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
|
||||||
|
iconId = R.drawable.ic_heart_24,
|
||||||
|
linkId = R.string.donate_url
|
||||||
|
)
|
||||||
|
|
||||||
|
if (FeatureFlags.internalUser()) {
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_preferences),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_internalSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BioPreference(val recipient: Recipient, val onClick: () -> Unit) : PreferenceModel<BioPreference>() {
|
||||||
|
override fun areContentsTheSame(newItem: BioPreference): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(newItem: BioPreference): Boolean {
|
||||||
|
return recipient == newItem.recipient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BioPreferenceViewHolder(itemView: View) : PreferenceViewHolder<BioPreference>(itemView) {
|
||||||
|
|
||||||
|
private val avatarView: AvatarImageView = itemView.findViewById(R.id.icon)
|
||||||
|
private val aboutView: TextView = itemView.findViewById(R.id.about)
|
||||||
|
|
||||||
|
override fun bind(model: BioPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
|
||||||
|
itemView.setOnClickListener { model.onClick() }
|
||||||
|
|
||||||
|
titleView.text = model.recipient.profileName.toString()
|
||||||
|
summaryView.text = PhoneNumberFormatter.prettyPrint(model.recipient.requireE164())
|
||||||
|
avatarView.setRecipient(Recipient.self())
|
||||||
|
|
||||||
|
titleView.visibility = View.VISIBLE
|
||||||
|
summaryView.visibility = View.VISIBLE
|
||||||
|
avatarView.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
if (model.recipient.combinedAboutAndEmoji != null) {
|
||||||
|
aboutView.text = model.recipient.combinedAboutAndEmoji
|
||||||
|
aboutView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
aboutView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PaymentsPreference(val unreadCount: Int, val onClick: () -> Unit) : PreferenceModel<PaymentsPreference>() {
|
||||||
|
override fun areContentsTheSame(newItem: PaymentsPreference): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && unreadCount == newItem.unreadCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(newItem: PaymentsPreference): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PaymentsPreferenceViewHolder(itemView: View) : MappingViewHolder<PaymentsPreference>(itemView) {
|
||||||
|
|
||||||
|
private val unreadCountView: TextView = itemView.findViewById(R.id.unread_indicator)
|
||||||
|
|
||||||
|
override fun bind(model: PaymentsPreference) {
|
||||||
|
unreadCountView.text = model.unreadCount.toString()
|
||||||
|
unreadCountView.visibility = if (model.unreadCount > 0) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
model.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
|
||||||
|
data class AppSettingsState(val self: Recipient, val unreadPaymentsCount: Int)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||||
|
|
||||||
|
class AppSettingsViewModel : ViewModel() {
|
||||||
|
|
||||||
|
val unreadPaymentsLiveData = UnreadPaymentsLiveData()
|
||||||
|
val selfLiveData: LiveData<Recipient> = Recipient.self().live().liveData
|
||||||
|
|
||||||
|
val state: LiveData<AppSettingsState> = LiveDataUtil.combineLatest(unreadPaymentsLiveData, selfLiveData) { payments, self ->
|
||||||
|
val unreadPaymentsCount = payments.transform { it.unreadCount }.or(0)
|
||||||
|
|
||||||
|
AppSettingsState(self, unreadPaymentsCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.account
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.InputType
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
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.navigation.Navigation
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.lock.PinHashing
|
||||||
|
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.util.ServiceUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||||
|
|
||||||
|
class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFragment__account) {
|
||||||
|
|
||||||
|
lateinit var viewModel: AccountSettingsViewModel
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
|
||||||
|
Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).setTextColor(Color.WHITE).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
viewModel = ViewModelProviders.of(this)[AccountSettingsViewModel::class.java]
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
|
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: AccountSettingsState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences_app_protection__signal_pin)
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
if (state.hasPin) {
|
||||||
|
startActivityForResult(CreateKbsPinActivity.getIntentForPinChangeFromSettings(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN)
|
||||||
|
} else {
|
||||||
|
startActivityForResult(CreateKbsPinActivity.getIntentForPinCreate(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_app_protection__pin_reminders),
|
||||||
|
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__youll_be_asked_less_frequently),
|
||||||
|
isChecked = state.pinRemindersEnabled,
|
||||||
|
onClick = {
|
||||||
|
setPinRemindersEnabled(!state.pinRemindersEnabled)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_app_protection__registration_lock),
|
||||||
|
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__require_your_signal_pin),
|
||||||
|
isChecked = state.registrationLockEnabled,
|
||||||
|
onClick = {
|
||||||
|
setRegistrationLockEnabled(!state.registrationLockEnabled)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__advanced_pin_settings),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.AccountSettingsFragment__account)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), R.color.signal_alert_primary)),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setRegistrationLockEnabled(enabled: Boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
RegistrationLockV2Dialog.showEnableDialog(requireContext()) { viewModel.refreshState() }
|
||||||
|
} else {
|
||||||
|
RegistrationLockV2Dialog.showDisableDialog(requireContext()) { viewModel.refreshState() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPinRemindersEnabled(enabled: Boolean) {
|
||||||
|
if (!enabled) {
|
||||||
|
val context: Context = requireContext()
|
||||||
|
val metrics: DisplayMetrics = resources.displayMetrics
|
||||||
|
|
||||||
|
val dialog: AlertDialog = AlertDialog.Builder(context, if (ThemeUtil.isDarkTheme(context)) R.style.Theme_Signal_AlertDialog_Dark_Cornered_ColoredAccent else R.style.Theme_Signal_AlertDialog_Light_Cornered_ColoredAccent)
|
||||||
|
.setView(R.layout.pin_disable_reminders_dialog)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
dialog.window!!.setLayout((metrics.widthPixels * .80).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
|
||||||
|
val pinEditText = DialogCompat.requireViewById(dialog, R.id.reminder_disable_pin) as EditText
|
||||||
|
val statusText = DialogCompat.requireViewById(dialog, R.id.reminder_disable_status) as TextView
|
||||||
|
val cancelButton = DialogCompat.requireViewById(dialog, R.id.reminder_disable_cancel)
|
||||||
|
val turnOffButton = DialogCompat.requireViewById(dialog, R.id.reminder_disable_turn_off)
|
||||||
|
|
||||||
|
pinEditText.post {
|
||||||
|
if (pinEditText.requestFocus()) {
|
||||||
|
ServiceUtil.getInputMethodManager(pinEditText.context).showSoftInput(pinEditText, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewCompat.setAutofillHints(pinEditText, HintConstants.AUTOFILL_HINT_PASSWORD)
|
||||||
|
|
||||||
|
when (SignalStore.pinValues().keyboardType) {
|
||||||
|
PinKeyboardType.NUMERIC -> pinEditText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||||
|
PinKeyboardType.ALPHA_NUMERIC -> pinEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
pinEditText.addTextChangedListener(object : SimpleTextWatcher() {
|
||||||
|
override fun onTextChanged(text: String) {
|
||||||
|
turnOffButton.isEnabled = text.length >= KbsConstants.MINIMUM_PIN_LENGTH
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pinEditText.typeface = Typeface.DEFAULT
|
||||||
|
turnOffButton.setOnClickListener {
|
||||||
|
val pin = pinEditText.text.toString()
|
||||||
|
val correct = PinHashing.verifyLocalPinHash(SignalStore.kbsValues().localPinHash!!, pin)
|
||||||
|
if (correct) {
|
||||||
|
SignalStore.pinValues().setPinRemindersEnabled(false)
|
||||||
|
viewModel.refreshState()
|
||||||
|
dialog.dismiss()
|
||||||
|
} else {
|
||||||
|
statusText.setText(R.string.preferences_app_protection__incorrect_pin_try_again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelButton.setOnClickListener { dialog.dismiss() }
|
||||||
|
} else {
|
||||||
|
SignalStore.pinValues().setPinRemindersEnabled(true)
|
||||||
|
viewModel.refreshState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.account
|
||||||
|
|
||||||
|
data class AccountSettingsState(
|
||||||
|
val hasPin: Boolean,
|
||||||
|
val pinRemindersEnabled: Boolean,
|
||||||
|
val registrationLockEnabled: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.account
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
|
||||||
|
class AccountSettingsViewModel : ViewModel() {
|
||||||
|
private val store: Store<AccountSettingsState> = Store(getCurrentState())
|
||||||
|
|
||||||
|
val state: LiveData<AccountSettingsState> = store.stateLiveData
|
||||||
|
|
||||||
|
fun refreshState() {
|
||||||
|
store.update { getCurrentState() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrentState(): AccountSettingsState {
|
||||||
|
return AccountSettingsState(
|
||||||
|
hasPin = SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().hasOptedOut(),
|
||||||
|
pinRemindersEnabled = SignalStore.pinValues().arePinRemindersEnabled(),
|
||||||
|
registrationLockEnabled = SignalStore.kbsValues().isV2RegistrationLockEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.appearance
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
|
||||||
|
class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__appearance) {
|
||||||
|
|
||||||
|
private lateinit var viewModel: AppearanceSettingsViewModel
|
||||||
|
|
||||||
|
private val themeLabels by lazy { resources.getStringArray(R.array.pref_theme_entries) }
|
||||||
|
private val themeValues by lazy { resources.getStringArray(R.array.pref_theme_values) }
|
||||||
|
|
||||||
|
private val messageFontSizeLabels by lazy { resources.getStringArray(R.array.pref_message_font_size_entries) }
|
||||||
|
private val messageFontSizeValues by lazy { resources.getStringArray(R.array.pref_message_font_size_values) }
|
||||||
|
|
||||||
|
private val languageLabels by lazy { resources.getStringArray(R.array.language_entries) }
|
||||||
|
private val languageValues by lazy { resources.getStringArray(R.array.language_values) }
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
viewModel = ViewModelProviders.of(this)[AppearanceSettingsViewModel::class.java]
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
|
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: AppearanceSettingsState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
radioListPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__theme),
|
||||||
|
listItems = themeLabels,
|
||||||
|
selected = themeValues.indexOf(state.theme),
|
||||||
|
onSelected = {
|
||||||
|
viewModel.setTheme(themeValues[it])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__chat_wallpaper),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_appearanceSettings_to_wallpaperActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
radioListPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_chats__message_text_size),
|
||||||
|
listItems = messageFontSizeLabels,
|
||||||
|
selected = messageFontSizeValues.indexOf(state.messageFontSize.toString()),
|
||||||
|
onSelected = {
|
||||||
|
viewModel.setMessageFontSize(messageFontSizeValues[it].toInt())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
radioListPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__language),
|
||||||
|
listItems = languageLabels,
|
||||||
|
selected = languageValues.indexOf(state.language),
|
||||||
|
onSelected = {
|
||||||
|
viewModel.setLanguage(languageValues[it])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.appearance
|
||||||
|
|
||||||
|
data class AppearanceSettingsState(
|
||||||
|
val theme: String,
|
||||||
|
val messageFontSize: Int,
|
||||||
|
val language: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.appearance
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
|
||||||
|
class AppearanceSettingsViewModel : ViewModel() {
|
||||||
|
private val store: Store<AppearanceSettingsState>
|
||||||
|
|
||||||
|
init {
|
||||||
|
val initialState = AppearanceSettingsState(
|
||||||
|
SignalStore.settings().theme,
|
||||||
|
SignalStore.settings().messageFontSize,
|
||||||
|
SignalStore.settings().language
|
||||||
|
)
|
||||||
|
|
||||||
|
store = Store(initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
val state: LiveData<AppearanceSettingsState> = store.stateLiveData
|
||||||
|
|
||||||
|
fun setTheme(theme: String) {
|
||||||
|
store.update { it.copy(theme = theme) }
|
||||||
|
SignalStore.settings().theme = theme
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLanguage(language: String) {
|
||||||
|
store.update { it.copy(language = language) }
|
||||||
|
SignalStore.settings().language = language
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMessageFontSize(size: Int) {
|
||||||
|
store.update { it.copy(messageFontSize = size) }
|
||||||
|
SignalStore.settings().messageFontSize = size
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.chats
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
|
||||||
|
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
|
||||||
|
|
||||||
|
private lateinit var viewModel: ChatsSettingsViewModel
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
val repository = ChatsSettingsRepository()
|
||||||
|
val factory = ChatsSettingsViewModel.Factory(repository)
|
||||||
|
viewModel = ViewModelProviders.of(this, factory)[ChatsSettingsViewModel::class.java]
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) {
|
||||||
|
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: ChatsSettingsState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__sms_mms),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_chatsSettingsFragment_to_smsSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__generate_link_previews),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__retrieve_link_previews_from_websites_for_messages),
|
||||||
|
isChecked = state.generateLinkPreviews,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setGenerateLinkPreviewsEnabled(!state.generateLinkPreviews)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__pref_use_address_book_photos),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__display_contact_photos_from_your_address_book_if_available),
|
||||||
|
isChecked = state.useAddressBook,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setUseAddressBook(!state.useAddressBook)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.ChatsSettingsFragment__keyboard)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_advanced__use_system_emoji),
|
||||||
|
isChecked = state.useSystemEmoji,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setUseSystemEmoji(!state.useSystemEmoji)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__enter_key_sends),
|
||||||
|
isChecked = state.enterKeySends,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setEnterKeySends(!state.enterKeySends)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences_chats__backups)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_chats__chat_backups),
|
||||||
|
summary = DSLSettingsText.from(if (state.chatBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.chats
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
|
||||||
|
class ChatsSettingsRepository {
|
||||||
|
|
||||||
|
private val context: Context = ApplicationDependencies.getApplication()
|
||||||
|
|
||||||
|
fun syncLinkPreviewsState() {
|
||||||
|
SignalExecutors.BOUNDED.execute {
|
||||||
|
val isLinkPreviewsEnabled = SignalStore.settings().isLinkPreviewsEnabled
|
||||||
|
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
|
ApplicationDependencies.getJobManager().add(
|
||||||
|
MultiDeviceConfigurationUpdateJob(
|
||||||
|
TextSecurePreferences.isReadReceiptsEnabled(context),
|
||||||
|
TextSecurePreferences.isTypingIndicatorsEnabled(context),
|
||||||
|
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
|
||||||
|
isLinkPreviewsEnabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (isLinkPreviewsEnabled) {
|
||||||
|
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.LINK_PREVIEWS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.chats
|
||||||
|
|
||||||
|
data class ChatsSettingsState(
|
||||||
|
val generateLinkPreviews: Boolean,
|
||||||
|
val useAddressBook: Boolean,
|
||||||
|
val useSystemEmoji: Boolean,
|
||||||
|
val enterKeySends: Boolean,
|
||||||
|
val chatBackupsEnabled: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.chats
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ThrottledDebouncer
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
|
||||||
|
class ChatsSettingsViewModel(private val repository: ChatsSettingsRepository) : ViewModel() {
|
||||||
|
|
||||||
|
private val refreshDebouncer = ThrottledDebouncer(500L)
|
||||||
|
|
||||||
|
private val store: Store<ChatsSettingsState> = Store(
|
||||||
|
ChatsSettingsState(
|
||||||
|
generateLinkPreviews = SignalStore.settings().isLinkPreviewsEnabled,
|
||||||
|
useAddressBook = SignalStore.settings().isPreferSystemContactPhotos,
|
||||||
|
useSystemEmoji = SignalStore.settings().isPreferSystemEmoji,
|
||||||
|
enterKeySends = SignalStore.settings().isEnterKeySends,
|
||||||
|
chatBackupsEnabled = SignalStore.settings().isBackupEnabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val state: LiveData<ChatsSettingsState> = store.stateLiveData
|
||||||
|
|
||||||
|
fun setGenerateLinkPreviewsEnabled(enabled: Boolean) {
|
||||||
|
store.update { it.copy(generateLinkPreviews = enabled) }
|
||||||
|
SignalStore.settings().isLinkPreviewsEnabled = enabled
|
||||||
|
repository.syncLinkPreviewsState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUseAddressBook(enabled: Boolean) {
|
||||||
|
store.update { it.copy(useAddressBook = enabled) }
|
||||||
|
SignalStore.settings().isPreferSystemContactPhotos = enabled
|
||||||
|
refreshDebouncer.publish { ConversationUtil.refreshRecipientShortcuts() }
|
||||||
|
ApplicationDependencies.getJobManager().add(MultiDeviceContactUpdateJob(true))
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUseSystemEmoji(enabled: Boolean) {
|
||||||
|
store.update { it.copy(useSystemEmoji = enabled) }
|
||||||
|
SignalStore.settings().isPreferSystemEmoji = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEnterKeySends(enabled: Boolean) {
|
||||||
|
store.update { it.copy(enterKeySends = enabled) }
|
||||||
|
SignalStore.settings().isEnterKeySends = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(private val repository: ChatsSettingsRepository) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return requireNotNull(modelClass.cast(ChatsSettingsViewModel(repository)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.chats.sms
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.SmsUtil
|
||||||
|
import org.thoughtcrime.securesms.util.Util
|
||||||
|
|
||||||
|
private const val SMS_REQUEST_CODE: Short = 1234
|
||||||
|
|
||||||
|
class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||||
|
|
||||||
|
private lateinit var viewModel: SmsSettingsViewModel
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.checkSmsEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
viewModel = ViewModelProviders.of(this)[SmsSettingsViewModel::class.java]
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) {
|
||||||
|
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(requireContext()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: SmsSettingsState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
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),
|
||||||
|
onClick = {
|
||||||
|
if (state.useAsDefaultSmsApp) {
|
||||||
|
startDefaultAppSelectionIntent()
|
||||||
|
} else {
|
||||||
|
startActivityForResult(SmsUtil.getSmsRoleIntent(requireContext()), SMS_REQUEST_CODE.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__sms_delivery_reports),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__request_a_delivery_report_for_each_sms_message_you_send),
|
||||||
|
isChecked = state.smsDeliveryReportsEnabled,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setSmsDeliveryReportsEnabled(!state.smsDeliveryReportsEnabled)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__support_wifi_calling),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__enable_if_your_device_supports_sms_mms_delivery_over_wifi),
|
||||||
|
isChecked = state.wifiCallingCompatibilityEnabled,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setWifiCallingCompatibilityEnabled(!state.wifiCallingCompatibilityEnabled)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__advanced_mms_access_point_names),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_smsSettingsFragment_to_mmsPreferencesFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linter isn't smart enough to figure out the else only happens if API >= 24
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
private fun startDefaultAppSelectionIntent() {
|
||||||
|
val intent: Intent = when {
|
||||||
|
Build.VERSION.SDK_INT < 23 -> Intent(Settings.ACTION_WIRELESS_SETTINGS)
|
||||||
|
Build.VERSION.SDK_INT < 24 -> Intent(Settings.ACTION_SETTINGS)
|
||||||
|
else -> Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivityForResult(intent, SMS_REQUEST_CODE.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.chats.sms
|
||||||
|
|
||||||
|
data class SmsSettingsState(
|
||||||
|
val useAsDefaultSmsApp: Boolean,
|
||||||
|
val smsDeliveryReportsEnabled: Boolean,
|
||||||
|
val wifiCallingCompatibilityEnabled: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.chats.sms
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.Util
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
|
||||||
|
class SmsSettingsViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val store = Store(
|
||||||
|
SmsSettingsState(
|
||||||
|
useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()),
|
||||||
|
smsDeliveryReportsEnabled = SignalStore.settings().isSmsDeliveryReportsEnabled,
|
||||||
|
wifiCallingCompatibilityEnabled = SignalStore.settings().isWifiCallingCompatibilityModeEnabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val state: LiveData<SmsSettingsState> = store.stateLiveData
|
||||||
|
|
||||||
|
fun setSmsDeliveryReportsEnabled(enabled: Boolean) {
|
||||||
|
store.update { it.copy(smsDeliveryReportsEnabled = enabled) }
|
||||||
|
SignalStore.settings().isSmsDeliveryReportsEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWifiCallingCompatibilityEnabled(enabled: Boolean) {
|
||||||
|
store.update { it.copy(wifiCallingCompatibilityEnabled = enabled) }
|
||||||
|
SignalStore.settings().isWifiCallingCompatibilityModeEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkSmsEnabled() {
|
||||||
|
store.update { it.copy(useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication())) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.data
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.util.Util
|
||||||
|
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences__data_and_storage) {
|
||||||
|
|
||||||
|
private val autoDownloadValues by lazy { resources.getStringArray(R.array.pref_media_download_entries) }
|
||||||
|
private val autoDownloadLabels by lazy { resources.getStringArray(R.array.pref_media_download_values) }
|
||||||
|
|
||||||
|
private val callBandwidthLabels by lazy { resources.getStringArray(R.array.pref_data_and_storage_call_bandwidth_values) }
|
||||||
|
|
||||||
|
private lateinit var viewModel: DataAndStorageSettingsViewModel
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
val repository = DataAndStorageSettingsRepository()
|
||||||
|
val factory = DataAndStorageSettingsViewModel.Factory(preferences, repository)
|
||||||
|
viewModel = ViewModelProviders.of(this, factory)[DataAndStorageSettingsViewModel::class.java]
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) {
|
||||||
|
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConfiguration(state: DataAndStorageSettingsState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_data_and_storage__manage_storage),
|
||||||
|
summary = DSLSettingsText.from(Util.getPrettyFileSize(state.totalStorageUse)),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_dataAndStorageSettingsFragment_to_storagePreferenceFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences_chats__media_auto_download)
|
||||||
|
|
||||||
|
multiSelectPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_chats__when_using_mobile_data),
|
||||||
|
listItems = autoDownloadLabels,
|
||||||
|
selected = autoDownloadValues.map { state.mobileAutoDownloadValues.contains(it) }.toBooleanArray(),
|
||||||
|
onSelected = {
|
||||||
|
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
|
||||||
|
viewModel.setMobileAutoDownloadValues(resultSet)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
multiSelectPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_chats__when_using_wifi),
|
||||||
|
listItems = autoDownloadLabels,
|
||||||
|
selected = autoDownloadValues.map { state.wifiAutoDownloadValues.contains(it) }.toBooleanArray(),
|
||||||
|
onSelected = {
|
||||||
|
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
|
||||||
|
viewModel.setWifiAutoDownloadValues(resultSet)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
multiSelectPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_chats__when_roaming),
|
||||||
|
listItems = autoDownloadLabels,
|
||||||
|
selected = autoDownloadValues.map { state.roamingAutoDownloadValues.contains(it) }.toBooleanArray(),
|
||||||
|
onSelected = {
|
||||||
|
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
|
||||||
|
viewModel.setRoamingAutoDownloadValues(resultSet)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__calls)
|
||||||
|
|
||||||
|
radioListPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_data_and_storage__use_less_data_for_calls),
|
||||||
|
listItems = callBandwidthLabels,
|
||||||
|
selected = abs(state.callBandwidthMode.code - 2),
|
||||||
|
onSelected = {
|
||||||
|
viewModel.setCallBandwidthMode(CallBandwidthMode.fromCode(abs(it - 2)))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
textPref(
|
||||||
|
summary = DSLSettingsText.from(R.string.preference_data_and_storage__using_less_data_may_improve_calls_on_bad_networks)
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences_proxy)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences_use_proxy),
|
||||||
|
summary = DSLSettingsText.from(if (state.isProxyEnabled) R.string.preferences_on else R.string.preferences_off),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_dataAndStorageSettingsFragment_to_editProxyFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
|
||||||
|
class DataAndStorageSettingsRepository {
|
||||||
|
|
||||||
|
private val context: Context = ApplicationDependencies.getApplication()
|
||||||
|
|
||||||
|
fun getTotalStorageUse(consumer: (Long) -> Unit) {
|
||||||
|
SignalExecutors.BOUNDED.execute {
|
||||||
|
val breakdown = DatabaseFactory.getMediaDatabase(context).storageBreakdown
|
||||||
|
|
||||||
|
consumer(listOf(breakdown.audioSize, breakdown.documentSize, breakdown.photoSize, breakdown.videoSize).sum())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.data
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
||||||
|
|
||||||
|
data class DataAndStorageSettingsState(
|
||||||
|
val totalStorageUse: Long,
|
||||||
|
val mobileAutoDownloadValues: Set<String>,
|
||||||
|
val wifiAutoDownloadValues: Set<String>,
|
||||||
|
val roamingAutoDownloadValues: Set<String>,
|
||||||
|
val callBandwidthMode: CallBandwidthMode,
|
||||||
|
val isProxyEnabled: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.data
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
||||||
|
|
||||||
|
class DataAndStorageSettingsViewModel(
|
||||||
|
private val sharedPreferences: SharedPreferences,
|
||||||
|
private val repository: DataAndStorageSettingsRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val store = Store(getState())
|
||||||
|
|
||||||
|
val state: LiveData<DataAndStorageSettingsState> = store.stateLiveData
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
repository.getTotalStorageUse { totalStorageUse ->
|
||||||
|
store.update { getState().copy(totalStorageUse = totalStorageUse) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMobileAutoDownloadValues(resultSet: Set<String>) {
|
||||||
|
sharedPreferences.edit().putStringSet(TextSecurePreferences.MEDIA_DOWNLOAD_MOBILE_PREF, resultSet).apply()
|
||||||
|
getStateAndCopyStorageUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWifiAutoDownloadValues(resultSet: Set<String>) {
|
||||||
|
sharedPreferences.edit().putStringSet(TextSecurePreferences.MEDIA_DOWNLOAD_WIFI_PREF, resultSet).apply()
|
||||||
|
getStateAndCopyStorageUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRoamingAutoDownloadValues(resultSet: Set<String>) {
|
||||||
|
sharedPreferences.edit().putStringSet(TextSecurePreferences.MEDIA_DOWNLOAD_ROAMING_PREF, resultSet).apply()
|
||||||
|
getStateAndCopyStorageUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCallBandwidthMode(callBandwidthMode: CallBandwidthMode) {
|
||||||
|
SignalStore.settings().callBandwidthMode = callBandwidthMode
|
||||||
|
ApplicationDependencies.getSignalCallManager().bandwidthModeUpdate()
|
||||||
|
getStateAndCopyStorageUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStateAndCopyStorageUsage() {
|
||||||
|
store.update { getState().copy(totalStorageUse = it.totalStorageUse) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getState() = DataAndStorageSettingsState(
|
||||||
|
totalStorageUse = 0,
|
||||||
|
mobileAutoDownloadValues = TextSecurePreferences.getMobileMediaDownloadAllowed(
|
||||||
|
ApplicationDependencies.getApplication()
|
||||||
|
),
|
||||||
|
wifiAutoDownloadValues = TextSecurePreferences.getWifiMediaDownloadAllowed(
|
||||||
|
ApplicationDependencies.getApplication()
|
||||||
|
),
|
||||||
|
roamingAutoDownloadValues = TextSecurePreferences.getRoamingMediaDownloadAllowed(
|
||||||
|
ApplicationDependencies.getApplication()
|
||||||
|
),
|
||||||
|
callBandwidthMode = SignalStore.settings().callBandwidthMode,
|
||||||
|
isProxyEnabled = SignalStore.proxy().isProxyEnabled
|
||||||
|
)
|
||||||
|
|
||||||
|
class Factory(
|
||||||
|
private val sharedPreferences: SharedPreferences,
|
||||||
|
private val repository: DataAndStorageSettingsRepository
|
||||||
|
) :
|
||||||
|
ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return requireNotNull(modelClass.cast(DataAndStorageSettingsViewModel(sharedPreferences, repository)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.help
|
||||||
|
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
|
||||||
|
class HelpSettingsFragment : DSLSettingsFragment(R.string.preferences__help) {
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
adapter.submitList(getConfiguration().toMappingModelList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConfiguration(): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
externalLinkPref(
|
||||||
|
title = DSLSettingsText.from(R.string.HelpSettingsFragment__support_center),
|
||||||
|
linkId = R.string.support_center_url
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.HelpSettingsFragment__contact_us),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_helpSettingsFragment_to_helpFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
textPref(
|
||||||
|
title = DSLSettingsText.from(R.string.HelpSettingsFragment__version),
|
||||||
|
summary = DSLSettingsText.from(BuildConfig.VERSION_NAME)
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.HelpSettingsFragment__debug_log),
|
||||||
|
onClick = {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.action_helpSettingsFragment_to_submitDebugLogActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
externalLinkPref(
|
||||||
|
title = DSLSettingsText.from(R.string.HelpSettingsFragment__terms_amp_privacy_policy),
|
||||||
|
linkId = R.string.terms_and_privacy_policy_url
|
||||||
|
)
|
||||||
|
|
||||||
|
textPref(
|
||||||
|
summary = DSLSettingsText.from(
|
||||||
|
StringBuilder().apply {
|
||||||
|
append(getString(R.string.HelpFragment__copyright_signal_messenger))
|
||||||
|
append("\n")
|
||||||
|
append(getString(R.string.HelpFragment__licenced_under_the_gplv3))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.internal
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||||
|
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||||
|
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
|
||||||
|
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
|
||||||
|
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
|
||||||
|
import org.thoughtcrime.securesms.payments.DataExportUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
|
||||||
|
|
||||||
|
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
|
||||||
|
|
||||||
|
private lateinit var viewModel: InternalSettingsViewModel
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
val repository = InternalSettingsRepository(requireContext())
|
||||||
|
val factory = InternalSettingsViewModel.Factory(repository)
|
||||||
|
viewModel = ViewModelProviders.of(this, factory)[InternalSettingsViewModel::class.java]
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) {
|
||||||
|
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: InternalSettingsState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_payments)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_payment_copy_data),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_payment_copy_data_description),
|
||||||
|
onClick = {
|
||||||
|
copyPaymentsDataToClipboard()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_account)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_refresh_attributes),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_refresh_attributes_description),
|
||||||
|
onClick = {
|
||||||
|
refreshAttributes()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_rotate_profile_key),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_rotate_profile_key_description),
|
||||||
|
onClick = {
|
||||||
|
rotateProfileKey()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_refresh_remote_values),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_refresh_remote_values_description),
|
||||||
|
onClick = {
|
||||||
|
refreshRemoteValues()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_display)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_user_details),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_user_details_description),
|
||||||
|
isChecked = state.seeMoreUserDetails,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setSeeMoreUserDetails(!state.seeMoreUserDetails)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_storage_service)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_force_storage_service_sync),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_force_storage_service_sync_description),
|
||||||
|
onClick = {
|
||||||
|
forceStorageServiceSync()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_preferences_groups_v2)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_do_not_create_gv2),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_create_gv2_description),
|
||||||
|
isChecked = state.gv2doNotCreateGv2Groups,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setGv2DoNotCreateGv2Groups(!state.gv2doNotCreateGv2Groups)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites_description),
|
||||||
|
isChecked = state.gv2forceInvites,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setGv2ForceInvites(!state.gv2forceInvites)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_ignore_gv2_server_changes),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_ignore_gv2_server_changes_description),
|
||||||
|
isChecked = state.gv2ignoreServerChanges,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setGv2IgnoreServerChanges(!state.gv2ignoreServerChanges)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_ignore_gv2_p2p_changes),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_ignore_gv2_server_changes_description),
|
||||||
|
isChecked = state.gv2ignoreP2PChanges,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setGv2IgnoreP2PChanges(!state.gv2ignoreP2PChanges)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_preferences_groups_v1_migration)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_do_not_initiate_automigrate),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_initiate_automigrate_description),
|
||||||
|
isChecked = state.disableAutoMigrationInitiation,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setDisableAutoMigrationInitiation(!state.disableAutoMigrationInitiation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_do_not_notify_automigrate),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_notify_automigrate_description),
|
||||||
|
isChecked = state.disableAutoMigrationNotification,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setDisableAutoMigrationNotification(!state.disableAutoMigrationNotification)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_network)
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_force_censorship),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_force_censorship_description),
|
||||||
|
isChecked = state.forceCensorship,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setDisableAutoMigrationNotification(!state.forceCensorship)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_conversations_and_shortcuts)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_delete_all_dynamic_shortcuts),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_dynamic_shortcuts),
|
||||||
|
onClick = {
|
||||||
|
deleteAllDynamicShortcuts()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_emoji)
|
||||||
|
|
||||||
|
val emojiSummary = if (state.emojiVersion == null) {
|
||||||
|
getString(R.string.preferences__internal_use_built_in_emoji_set)
|
||||||
|
} else {
|
||||||
|
getString(
|
||||||
|
R.string.preferences__internal_current_version_d_at_density_s,
|
||||||
|
state.emojiVersion.version,
|
||||||
|
state.emojiVersion.density
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_use_built_in_emoji_set),
|
||||||
|
summary = DSLSettingsText.from(emojiSummary),
|
||||||
|
isChecked = state.useBuiltInEmojiSet,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setDisableAutoMigrationNotification(!state.useBuiltInEmojiSet)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyPaymentsDataToClipboard() {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setMessage(
|
||||||
|
"""
|
||||||
|
Local payments history will be copied to the clipboard.
|
||||||
|
It may therefore compromise privacy.
|
||||||
|
However, no private keys will be copied.
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
.setPositiveButton(
|
||||||
|
"Copy"
|
||||||
|
) { _: DialogInterface?, _: Int ->
|
||||||
|
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)
|
||||||
|
null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Payments have been copied",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshAttributes() {
|
||||||
|
ApplicationDependencies.getJobManager()
|
||||||
|
.startChain(RefreshAttributesJob())
|
||||||
|
.then(RefreshOwnProfileJob())
|
||||||
|
.enqueue()
|
||||||
|
Toast.makeText(context, "Scheduled attribute refresh", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rotateProfileKey() {
|
||||||
|
ApplicationDependencies.getJobManager().add(RotateProfileKeyJob())
|
||||||
|
Toast.makeText(context, "Scheduled profile key rotation", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshRemoteValues() {
|
||||||
|
ApplicationDependencies.getJobManager().add(RemoteConfigRefreshJob())
|
||||||
|
Toast.makeText(context, "Scheduled remote config refresh", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forceStorageServiceSync() {
|
||||||
|
ApplicationDependencies.getJobManager().add(StorageForcePushJob())
|
||||||
|
Toast.makeText(context, "Scheduled storage force push", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteAllDynamicShortcuts() {
|
||||||
|
ConversationUtil.clearAllShortcuts(requireContext())
|
||||||
|
Toast.makeText(context, "Deleted all dynamic shortcuts.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.internal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiFiles
|
||||||
|
|
||||||
|
class InternalSettingsRepository(context: Context) {
|
||||||
|
|
||||||
|
private val context = context.applicationContext
|
||||||
|
|
||||||
|
fun getEmojiVersionInfo(consumer: (EmojiFiles.Version?) -> Unit) {
|
||||||
|
SignalExecutors.BOUNDED.execute {
|
||||||
|
consumer(EmojiFiles.Version.readVersion(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.internal
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiFiles
|
||||||
|
|
||||||
|
data class InternalSettingsState(
|
||||||
|
val seeMoreUserDetails: Boolean,
|
||||||
|
val gv2doNotCreateGv2Groups: Boolean,
|
||||||
|
val gv2forceInvites: Boolean,
|
||||||
|
val gv2ignoreServerChanges: Boolean,
|
||||||
|
val gv2ignoreP2PChanges: Boolean,
|
||||||
|
val disableAutoMigrationInitiation: Boolean,
|
||||||
|
val disableAutoMigrationNotification: Boolean,
|
||||||
|
val forceCensorship: Boolean,
|
||||||
|
val useBuiltInEmojiSet: Boolean,
|
||||||
|
val emojiVersion: EmojiFiles.Version?
|
||||||
|
)
|
||||||