mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-18 16:57:46 +00:00
Compare commits
253 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8857352b | ||
|
|
c09a1fdba8 | ||
|
|
cdc7033a51 | ||
|
|
fa30c759d7 | ||
|
|
d040be2df0 | ||
|
|
935c831a7f | ||
|
|
867e95eef1 | ||
|
|
2ee04bd1b6 | ||
|
|
75d567e555 | ||
|
|
d8a489971c | ||
|
|
19ce5b5c76 | ||
|
|
7c70ea4d3e | ||
|
|
2784285d47 | ||
|
|
c946a7a1d5 | ||
|
|
3e60b49b8b | ||
|
|
4e7331bbb8 | ||
|
|
b8c7e86223 | ||
|
|
3b925f8674 | ||
|
|
f1f6d41c73 | ||
|
|
29ef1cb1be | ||
|
|
4296085d65 | ||
|
|
c797b09228 | ||
|
|
a870ef0030 | ||
|
|
43ed9e7310 | ||
|
|
bcd27355f9 | ||
|
|
6a14dc69c0 | ||
|
|
ed9acd25f9 | ||
|
|
7b24e66ed3 | ||
|
|
abd3d4b546 | ||
|
|
4040c4240a | ||
|
|
1ee747f3ef | ||
|
|
f88874bec8 | ||
|
|
ed440a2150 | ||
|
|
2fd46b196b | ||
|
|
12dfcaf7e7 | ||
|
|
f4a199f621 | ||
|
|
bb708e0aa3 | ||
|
|
d625740ca4 | ||
|
|
250402e9b9 | ||
|
|
1d2ffe56fb | ||
|
|
d16c0d2887 | ||
|
|
b3555f2f94 | ||
|
|
83a638fc6d | ||
|
|
f1534a710f | ||
|
|
a16845340b | ||
|
|
ffa4725f8e | ||
|
|
7792c66c64 | ||
|
|
1a3985d709 | ||
|
|
4714895c59 | ||
|
|
1e37951701 | ||
|
|
e8be1ad752 | ||
|
|
e316a70b6c | ||
|
|
40a8d21c15 | ||
|
|
28d5ca7ed9 | ||
|
|
110b18545f | ||
|
|
a478605da4 | ||
|
|
f5f1589813 | ||
|
|
0c332b6adb | ||
|
|
ba712ce357 | ||
|
|
2d2395accf | ||
|
|
8634289b7a | ||
|
|
45043fb9a8 | ||
|
|
0449795725 | ||
|
|
a96093f1b7 | ||
|
|
bd4f7691e9 | ||
|
|
e12acbae70 | ||
|
|
48dc4eac10 | ||
|
|
a869c92eee | ||
|
|
4fefd14538 | ||
|
|
c09dbfa47c | ||
|
|
d3c9f66de6 | ||
|
|
01d7694108 | ||
|
|
1425b651d4 | ||
|
|
b1befbeefc | ||
|
|
3a9a84a0b1 | ||
|
|
368284cccc | ||
|
|
ef777f4db9 | ||
|
|
a8e4e8e882 | ||
|
|
cf93760d00 | ||
|
|
dd8b9ff8fb | ||
|
|
bfed03b7b5 | ||
|
|
860f06ec9e | ||
|
|
b58376920f | ||
|
|
4ace075ddf | ||
|
|
dda98a474d | ||
|
|
f1c0df7d87 | ||
|
|
c78e098cb4 | ||
|
|
a3438c4f8d | ||
|
|
92ecf2d5de | ||
|
|
f18b653725 | ||
|
|
5128438cfb | ||
|
|
f29f25822b | ||
|
|
ecfe218840 | ||
|
|
dd33d2b5d0 | ||
|
|
12a8d4e10b | ||
|
|
c5c2fb31b1 | ||
|
|
343b7faf98 | ||
|
|
18aa8bbf60 | ||
|
|
a358d1630f | ||
|
|
01375b321c | ||
|
|
d2739d52e0 | ||
|
|
4668510106 | ||
|
|
ffcd311c90 | ||
|
|
b94a636542 | ||
|
|
a7aec6bfbc | ||
|
|
190ca9eddd | ||
|
|
2cf9eb69eb | ||
|
|
ffcb90da52 | ||
|
|
878b0c9275 | ||
|
|
5505cb0dea | ||
|
|
7ac14dccda | ||
|
|
6cffd0a723 | ||
|
|
220ebf93c7 | ||
|
|
d0681a5592 | ||
|
|
09d167c16d | ||
|
|
477bb45df7 | ||
|
|
e006306036 | ||
|
|
065cbcf0f9 | ||
|
|
7a6b958bbe | ||
|
|
ef6a5b6599 | ||
|
|
cdae919b5e | ||
|
|
12889f4549 | ||
|
|
089d59b691 | ||
|
|
b3e247e9cc | ||
|
|
56392b87f7 | ||
|
|
1b1a4aeb38 | ||
|
|
16147e0c08 | ||
|
|
139317cf1b | ||
|
|
72b94127fb | ||
|
|
1f1fc94d22 | ||
|
|
a574fe026c | ||
|
|
aa82083d30 | ||
|
|
08d5df70c2 | ||
|
|
29b8fa5897 | ||
|
|
e96faf31d4 | ||
|
|
157a73aa99 | ||
|
|
bdd298c8a0 | ||
|
|
3f7dd21186 | ||
|
|
086b708cf7 | ||
|
|
57e0e57f48 | ||
|
|
4b7efbfdc0 | ||
|
|
7dc2653042 | ||
|
|
e428453835 | ||
|
|
f84c8229de | ||
|
|
a73427d68d | ||
|
|
e4456bb236 | ||
|
|
06eadd0c15 | ||
|
|
3c90dfa660 | ||
|
|
ace1b8ee71 | ||
|
|
676356e800 | ||
|
|
f732e54c22 | ||
|
|
cdc2e74f68 | ||
|
|
724f3e872b | ||
|
|
d63e5165eb | ||
|
|
9892c4392e | ||
|
|
5ced1a775c | ||
|
|
761de1318e | ||
|
|
02508512d5 | ||
|
|
6e6105af05 | ||
|
|
d569419e13 | ||
|
|
93f1641803 | ||
|
|
ff52bf93fa | ||
|
|
a039275a0c | ||
|
|
a98d10104d | ||
|
|
8924bc59b1 | ||
|
|
eefe60a9c9 | ||
|
|
fe1cb3d904 | ||
|
|
0448278a78 | ||
|
|
99c0c2ff4c | ||
|
|
b369b734ca | ||
|
|
57150a20fd | ||
|
|
1634d7d531 | ||
|
|
d563de4207 | ||
|
|
5cd4b82ed0 | ||
|
|
5f728d348c | ||
|
|
596c4b6e40 | ||
|
|
36d1e7c44a | ||
|
|
25c17082f2 | ||
|
|
810ccf8e94 | ||
|
|
c8ed0b19f0 | ||
|
|
9e09444c65 | ||
|
|
5923fa0cd5 | ||
|
|
b2d4c5d14b | ||
|
|
0bb9c1d650 | ||
|
|
fbfa3abffd | ||
|
|
b5656aa5dd | ||
|
|
d53fd6a109 | ||
|
|
b0650b926b | ||
|
|
845f6a0a93 | ||
|
|
d8daa83c79 | ||
|
|
7bb0199e83 | ||
|
|
f014dadf06 | ||
|
|
393e54ce91 | ||
|
|
fdf4ad9543 | ||
|
|
5f0d384c9e | ||
|
|
4271700046 | ||
|
|
e153b0ab78 | ||
|
|
26868ae668 | ||
|
|
17c0364eda | ||
|
|
b28ac7af8c | ||
|
|
2dcaa21a44 | ||
|
|
33cc8363f9 | ||
|
|
9b61e1c85c | ||
|
|
6f53fdc02d | ||
|
|
6f850f5a55 | ||
|
|
a482a4b1f4 | ||
|
|
3664e6f96d | ||
|
|
dda8808173 | ||
|
|
63a24c23cc | ||
|
|
1ec3a72f79 | ||
|
|
566285ec0e | ||
|
|
d5ba82338d | ||
|
|
cbecd2a2fc | ||
|
|
3772dd40ac | ||
|
|
f69a0f0261 | ||
|
|
cb323ffb84 | ||
|
|
0db73e71a0 | ||
|
|
eeb0c838db | ||
|
|
dc48ee5aed | ||
|
|
c0acfa57a9 | ||
|
|
3e166ef927 | ||
|
|
4942d83de5 | ||
|
|
4c30b39e71 | ||
|
|
e55f4fe6b6 | ||
|
|
aff74cffa0 | ||
|
|
8b29bb8664 | ||
|
|
3cee57b6c2 | ||
|
|
857f4a4fc8 | ||
|
|
a942293a74 | ||
|
|
550b121990 | ||
|
|
cc84901a49 | ||
|
|
9d3764c5d9 | ||
|
|
0950235ccd | ||
|
|
8ed7fc894e | ||
|
|
e504ffa225 | ||
|
|
9c63b37bb4 | ||
|
|
5c110ca359 | ||
|
|
1ab61beeb9 | ||
|
|
8e45a546c9 | ||
|
|
745a7f76ea | ||
|
|
8cb9ab3204 | ||
|
|
12533d1414 | ||
|
|
bd1c164d57 | ||
|
|
7446c2096d | ||
|
|
8ce5c4b885 | ||
|
|
ab76112f5f | ||
|
|
9c54e39eae | ||
|
|
61eab44474 | ||
|
|
f6285ec710 | ||
|
|
ed878ec4b4 | ||
|
|
e38d41d67a | ||
|
|
3d237d72bd | ||
|
|
8044d2390c |
@@ -83,7 +83,7 @@ There are several other ways to get involved:
|
||||
* Try to reproduce issues and help with troubleshooting.
|
||||
* Discover solutions to open issues and post any relevant findings.
|
||||
* Test other people's pull requests.
|
||||
* Contribute to Signal via the [Freedom of the Press Foundation's donation page](https://freedom.press/crowdfunding/signal/).
|
||||
* [Donate to Signal.](https://signal.org/donate/)
|
||||
* Share Signal with your friends and family.
|
||||
|
||||
Signal is made for you. Thank you for your feedback and support.
|
||||
|
||||
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 679
|
||||
def canonicalVersionName = "4.67.2"
|
||||
def canonicalVersionCode = 705
|
||||
def canonicalVersionName = "4.71.2"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -122,8 +122,9 @@ android {
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_SERVICE_ID", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
||||
@@ -163,6 +164,10 @@ android {
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!fssd_25_8bit_gray_v1.tflite:!fssd_25_8bit_v1.tflite:!blazeface.tfl'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled true
|
||||
@@ -197,8 +202,9 @@ android {
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"b657cad56d518827b0938949bb1e5727a9a4db358dd6a88e55e710a89ffa50bd\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
|
||||
buildConfigField "String", "KBS_SERVICE_ID", "\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
}
|
||||
@@ -304,7 +310,7 @@ dependencies {
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.3.1'
|
||||
implementation 'org.signal:ringrtc-android:2.5.1'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
@@ -358,11 +364,11 @@ dependencies {
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.9.5'
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
testImplementation 'org.mockito:mockito-core:2.8.9'
|
||||
testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.2') {
|
||||
@@ -455,3 +461,13 @@ def getLastCommitTimestamp() {
|
||||
return os.toString() + "000"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
testLogging {
|
||||
events "failed"
|
||||
exceptionFormat "full"
|
||||
showCauses true
|
||||
showExceptions true
|
||||
showStackTraces true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
<!-- L10N warnings -->
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
<issue id="MissingQuantity" severity="warning" />
|
||||
<issue id="MissingDefaultResource" severity="error">
|
||||
<ignore path="*/res/values-*/strings.xml" /> <!-- Ignore for non-English, excludeNonTranslatables task will remove these -->
|
||||
</issue>
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="ImpliedQuantity" severity="warning" />
|
||||
<issue id="TypographyDashes" severity="error" >
|
||||
<ignore path="*/res/values-*" /> <!-- Ignore for non-English -->
|
||||
<ignore path="*/res/values-*/strings.xml" /> <!-- Ignore for non-English -->
|
||||
</issue>
|
||||
|
||||
<issue id="CanvasSize" severity="error" />
|
||||
|
||||
@@ -15,7 +15,9 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -29,13 +31,23 @@ import java.util.Map;
|
||||
*/
|
||||
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
||||
|
||||
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
|
||||
|
||||
public FlipperSqlCipherAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Descriptor> getDatabases() {
|
||||
return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext())));
|
||||
try {
|
||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||
databaseHelperField.setAccessible(true);
|
||||
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
|
||||
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, "Unable to use reflection to access raw database.", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -127,13 +127,13 @@
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:screenOrientation="portrait"
|
||||
android:noHistory="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".InviteActivity"
|
||||
android:theme="@style/Signal.Light.NoActionBar.Invite"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
@@ -142,10 +142,10 @@
|
||||
<activity android:name=".PromptMmsActivity"
|
||||
android:label="Configure MMS Settings"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".DeviceProvisioningActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -155,7 +155,7 @@
|
||||
</activity>
|
||||
|
||||
<activity android:name=".preferences.MmsPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".sharing.ShareActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
@@ -164,7 +164,7 @@
|
||||
android:taskAffinity=""
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -195,7 +195,7 @@
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -223,6 +223,15 @@
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true"
|
||||
tools:targetApi="23">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https"
|
||||
android:host="signal.group"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
||||
@@ -233,7 +242,7 @@
|
||||
<activity android:name=".conversation.ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
@@ -248,78 +257,82 @@
|
||||
android:taskAffinity=""
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/TextSecure.LightTheme.Popup"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" />
|
||||
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:label="@string/AndroidManifest__message_details"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".migrations.ApplicationMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".PassphraseCreateActivity"
|
||||
android:label="@string/AndroidManifest__create_passphrase"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".PassphrasePromptActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightIntroTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".NewConversationActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".PushContactSelectionActivity"
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".giph.ui.GiphyActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".mediasend.MediaSendActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".VerifyIdentityActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".ApplicationPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.NOTIFICATION_PREFERENCES" />
|
||||
@@ -330,45 +343,45 @@
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".revealable.ViewOnceMessageActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:excludeFromRecents="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".stickers.StickerManagementActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".DeviceActivity"
|
||||
android:label="@string/AndroidManifest__linked_devices"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".logsubmit.SubmitDebugLogActivity"
|
||||
android:label="@string/AndroidManifest__log_submit"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".MediaPreviewActivity"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".AvatarPreviewActivity"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".DummyActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
@@ -383,7 +396,7 @@
|
||||
|
||||
<activity android:name=".PlayServicesProblemActivity"
|
||||
android:theme="@style/TextSecure.DialogActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".SmsSendtoActivity">
|
||||
<intent-filter>
|
||||
@@ -407,7 +420,7 @@
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -419,15 +432,15 @@
|
||||
|
||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".BlockedContactsActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
|
||||
android:theme="@style/TextSecure.DarkTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".profiles.edit.EditProfileActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
@@ -436,16 +449,16 @@
|
||||
<activity android:name=".lock.v2.CreateKbsPinActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".lock.v2.KbsMigrationActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".ClearProfileAvatarActivity"
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:icon="@drawable/clear_profile_avatar"
|
||||
android:label="@string/AndroidManifest_remove_photo">
|
||||
|
||||
@@ -455,42 +468,45 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert" />
|
||||
|
||||
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".contactshare.ContactShareEditActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".contactshare.ContactNameEditActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".contactshare.SharedContactDetailsActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".ShortcutLauncherActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity
|
||||
android:name=".maps.PlacePickerActivity"
|
||||
android:label="@string/PlacePickerActivity_title"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".MainActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".pin.PinRestoreActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
@@ -505,7 +521,12 @@
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".megaphone.ClientDeprecatedActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
@@ -624,6 +645,8 @@
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
|
||||
|
||||
<provider android:name=".providers.PartProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
|
||||
58
app/src/main/java/org/signal/glide/Log.java
Normal file
58
app/src/main/java/org/signal/glide/Log.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package org.signal.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class Log {
|
||||
|
||||
private Log() {}
|
||||
|
||||
public static void v(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().v(tag, message);
|
||||
}
|
||||
|
||||
public static void d(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().d(tag, message);
|
||||
}
|
||||
|
||||
public static void i(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().i(tag, message);
|
||||
}
|
||||
|
||||
public static void w(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().w(tag, message);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message) {
|
||||
e(tag, message, null);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
SignalGlideCodecs.getLogProvider().e(tag, message, throwable);
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
void v(@NonNull String tag, @NonNull String message);
|
||||
void d(@NonNull String tag, @NonNull String message);
|
||||
void i(@NonNull String tag, @NonNull String message);
|
||||
void w(@NonNull String tag, @NonNull String message);
|
||||
void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable);
|
||||
|
||||
Provider EMPTY = new Provider() {
|
||||
@Override
|
||||
public void v(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void d(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void i(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void w(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void e(@NonNull String tag, @NonNull String message, @NonNull Throwable throwable) { }
|
||||
};
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.signal.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class SignalGlideCodecs {
|
||||
|
||||
private static Log.Provider logProvider = Log.Provider.EMPTY;
|
||||
|
||||
private SignalGlideCodecs() {}
|
||||
|
||||
public static void setLogProvider(@NonNull Log.Provider provider) {
|
||||
logProvider = provider;
|
||||
}
|
||||
|
||||
public static @NonNull Log.Provider getLogProvider() {
|
||||
return logProvider;
|
||||
}
|
||||
}
|
||||
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.common.FrameAnimationDrawable;
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.AssetStreamLoader;
|
||||
import org.signal.glide.common.loader.FileLoader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
import org.signal.glide.common.loader.ResourceStreamLoader;
|
||||
|
||||
/**
|
||||
* @Description: APNGDrawable
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public class APNGDrawable extends FrameAnimationDrawable<APNGDecoder> {
|
||||
public APNGDrawable(Loader provider) {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
public APNGDrawable(APNGDecoder decoder) {
|
||||
super(decoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGDecoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener) {
|
||||
return new APNGDecoder(streamLoader, listener);
|
||||
}
|
||||
|
||||
|
||||
public static APNGDrawable fromAsset(Context context, String assetPath) {
|
||||
AssetStreamLoader assetStreamLoader = new AssetStreamLoader(context, assetPath);
|
||||
return new APNGDrawable(assetStreamLoader);
|
||||
}
|
||||
|
||||
public static APNGDrawable fromFile(String filePath) {
|
||||
FileLoader fileLoader = new FileLoader(filePath);
|
||||
return new APNGDrawable(fileLoader);
|
||||
}
|
||||
|
||||
public static APNGDrawable fromResource(Context context, int resId) {
|
||||
ResourceStreamLoader resourceStreamLoader = new ResourceStreamLoader(context, resId);
|
||||
return new APNGDrawable(resourceStreamLoader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27acTL.27:_The_Animation_Control_Chunk
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class ACTLChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("acTL");
|
||||
int num_frames;
|
||||
int num_plays;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader apngReader) throws IOException {
|
||||
num_frames = apngReader.readInt();
|
||||
num_plays = apngReader.readInt();
|
||||
}
|
||||
}
|
||||
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||
|
||||
private static final String TAG = APNGDecoder.class.getSimpleName();
|
||||
|
||||
private APNGWriter apngWriter;
|
||||
private int mLoopCount;
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
|
||||
private class SnapShot {
|
||||
byte dispose_op;
|
||||
Rect dstRect = new Rect();
|
||||
ByteBuffer byteBuffer;
|
||||
}
|
||||
|
||||
private SnapShot snapShot = new SnapShot();
|
||||
|
||||
/**
|
||||
* @param loader webp的reader
|
||||
* @param renderListener 渲染的回调
|
||||
*/
|
||||
public APNGDecoder(Loader loader, FrameSeqDecoder.RenderListener renderListener) {
|
||||
super(loader, renderListener);
|
||||
paint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGWriter getWriter() {
|
||||
if (apngWriter == null) {
|
||||
apngWriter = new APNGWriter();
|
||||
}
|
||||
return apngWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGReader getReader(Reader reader) {
|
||||
return new APNGReader(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLoopCount() {
|
||||
return mLoopCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release() {
|
||||
snapShot.byteBuffer = null;
|
||||
apngWriter = null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Rect read(APNGReader reader) throws IOException {
|
||||
List<Chunk> chunks = APNGParser.parse(reader);
|
||||
List<Chunk> otherChunks = new ArrayList<>();
|
||||
|
||||
boolean actl = false;
|
||||
APNGFrame lastFrame = null;
|
||||
byte[] ihdrData = new byte[0];
|
||||
int canvasWidth = 0, canvasHeight = 0;
|
||||
for (Chunk chunk : chunks) {
|
||||
if (chunk instanceof ACTLChunk) {
|
||||
mLoopCount = ((ACTLChunk) chunk).num_plays;
|
||||
actl = true;
|
||||
} else if (chunk instanceof FCTLChunk) {
|
||||
APNGFrame frame = new APNGFrame(reader, (FCTLChunk) chunk);
|
||||
frame.prefixChunks = otherChunks;
|
||||
frame.ihdrData = ihdrData;
|
||||
frames.add(frame);
|
||||
lastFrame = frame;
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
if (lastFrame != null) {
|
||||
lastFrame.imageChunks.add(chunk);
|
||||
}
|
||||
} else if (chunk instanceof IDATChunk) {
|
||||
if (!actl) {
|
||||
//如果为非APNG图片,则只解码PNG
|
||||
Frame frame = new StillFrame(reader);
|
||||
frame.frameWidth = canvasWidth;
|
||||
frame.frameHeight = canvasHeight;
|
||||
frames.add(frame);
|
||||
mLoopCount = 1;
|
||||
break;
|
||||
}
|
||||
if (lastFrame != null) {
|
||||
lastFrame.imageChunks.add(chunk);
|
||||
}
|
||||
|
||||
} else if (chunk instanceof IHDRChunk) {
|
||||
canvasWidth = ((IHDRChunk) chunk).width;
|
||||
canvasHeight = ((IHDRChunk) chunk).height;
|
||||
ihdrData = ((IHDRChunk) chunk).data;
|
||||
} else if (!(chunk instanceof IENDChunk)) {
|
||||
otherChunks.add(chunk);
|
||||
}
|
||||
}
|
||||
frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
return new Rect(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderFrame(Frame frame) {
|
||||
if (frame == null || fullRect == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Bitmap bitmap = obtainBitmap(fullRect.width() / sampleSize, fullRect.height() / sampleSize);
|
||||
Canvas canvas = cachedCanvas.get(bitmap);
|
||||
if (canvas == null) {
|
||||
canvas = new Canvas(bitmap);
|
||||
cachedCanvas.put(bitmap, canvas);
|
||||
}
|
||||
if (frame instanceof APNGFrame) {
|
||||
// 从缓存中恢复当前帧
|
||||
frameBuffer.rewind();
|
||||
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||
// 开始绘制前,处理快照中的设定
|
||||
if (this.frameIndex == 0) {
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
} else {
|
||||
canvas.save();
|
||||
canvas.clipRect(snapShot.dstRect);
|
||||
switch (snapShot.dispose_op) {
|
||||
// 从快照中恢复上一帧之前的显示内容
|
||||
case FCTLChunk.APNG_DISPOSE_OP_PREVIOUS:
|
||||
snapShot.byteBuffer.rewind();
|
||||
bitmap.copyPixelsFromBuffer(snapShot.byteBuffer);
|
||||
break;
|
||||
// 清空上一帧所画区域
|
||||
case FCTLChunk.APNG_DISPOSE_OP_BACKGROUND:
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
break;
|
||||
// 什么都不做
|
||||
case FCTLChunk.APNG_DISPOSE_OP_NON:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// 然后根据dispose设定传递到快照信息中
|
||||
if (((APNGFrame) frame).dispose_op == FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||
if (snapShot.dispose_op != FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||
snapShot.byteBuffer.rewind();
|
||||
bitmap.copyPixelsToBuffer(snapShot.byteBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
snapShot.dispose_op = ((APNGFrame) frame).dispose_op;
|
||||
canvas.save();
|
||||
if (((APNGFrame) frame).blend_op == FCTLChunk.APNG_BLEND_OP_SOURCE) {
|
||||
canvas.clipRect(
|
||||
frame.frameX / sampleSize,
|
||||
frame.frameY / sampleSize,
|
||||
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
}
|
||||
|
||||
|
||||
snapShot.dstRect.set(frame.frameX / sampleSize,
|
||||
frame.frameY / sampleSize,
|
||||
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||
canvas.restore();
|
||||
}
|
||||
//开始真正绘制当前帧的内容
|
||||
Bitmap inBitmap = obtainBitmap(frame.frameWidth, frame.frameHeight);
|
||||
recycleBitmap(frame.draw(canvas, paint, sampleSize, inBitmap, getWriter()));
|
||||
recycleBitmap(inBitmap);
|
||||
frameBuffer.rewind();
|
||||
bitmap.copyPixelsToBuffer(frameBuffer);
|
||||
recycleBitmap(bitmap);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "Failed to render!", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGFrame extends Frame<APNGReader, APNGWriter> {
|
||||
public final byte blend_op;
|
||||
public final byte dispose_op;
|
||||
byte[] ihdrData;
|
||||
List<Chunk> imageChunks = new ArrayList<>();
|
||||
List<Chunk> prefixChunks = new ArrayList<>();
|
||||
private static final byte[] sPNGSignatures = {(byte) 137, 80, 78, 71, 13, 10, 26, 10};
|
||||
private static final byte[] sPNGEndChunk = {0, 0, 0, 0, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82};
|
||||
|
||||
private static ThreadLocal<CRC32> sCRC32 = new ThreadLocal<>();
|
||||
|
||||
private CRC32 getCRC32() {
|
||||
CRC32 crc32 = sCRC32.get();
|
||||
if (crc32 == null) {
|
||||
crc32 = new CRC32();
|
||||
sCRC32.set(crc32);
|
||||
}
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public APNGFrame(APNGReader reader, FCTLChunk fctlChunk) {
|
||||
super(reader);
|
||||
blend_op = fctlChunk.blend_op;
|
||||
dispose_op = fctlChunk.dispose_op;
|
||||
frameDuration = fctlChunk.delay_num * 1000 / (fctlChunk.delay_den == 0 ? 100 : fctlChunk.delay_den);
|
||||
frameWidth = fctlChunk.width;
|
||||
frameHeight = fctlChunk.height;
|
||||
frameX = fctlChunk.x_offset;
|
||||
frameY = fctlChunk.y_offset;
|
||||
}
|
||||
|
||||
private int encode(APNGWriter apngWriter) throws IOException {
|
||||
int fileSize = 8 + 13 + 12;
|
||||
|
||||
//prefixChunks
|
||||
for (Chunk chunk : prefixChunks) {
|
||||
fileSize += chunk.length + 12;
|
||||
}
|
||||
|
||||
//imageChunks
|
||||
for (Chunk chunk : imageChunks) {
|
||||
if (chunk instanceof IDATChunk) {
|
||||
fileSize += chunk.length + 12;
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
fileSize += chunk.length + 8;
|
||||
}
|
||||
}
|
||||
fileSize += sPNGEndChunk.length;
|
||||
apngWriter.reset(fileSize);
|
||||
apngWriter.putBytes(sPNGSignatures);
|
||||
//IHDR Chunk
|
||||
apngWriter.writeInt(13);
|
||||
int start = apngWriter.position();
|
||||
apngWriter.writeFourCC(IHDRChunk.ID);
|
||||
apngWriter.writeInt(frameWidth);
|
||||
apngWriter.writeInt(frameHeight);
|
||||
apngWriter.putBytes(ihdrData);
|
||||
CRC32 crc32 = getCRC32();
|
||||
crc32.reset();
|
||||
crc32.update(apngWriter.toByteArray(), start, 17);
|
||||
apngWriter.writeInt((int) crc32.getValue());
|
||||
|
||||
//prefixChunks
|
||||
for (Chunk chunk : prefixChunks) {
|
||||
if (chunk instanceof IENDChunk) {
|
||||
continue;
|
||||
}
|
||||
reader.reset();
|
||||
reader.skip(chunk.offset);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||
apngWriter.skip(chunk.length + 12);
|
||||
}
|
||||
//imageChunks
|
||||
for (Chunk chunk : imageChunks) {
|
||||
if (chunk instanceof IDATChunk) {
|
||||
reader.reset();
|
||||
reader.skip(chunk.offset);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||
apngWriter.skip(chunk.length + 12);
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
apngWriter.writeInt(chunk.length - 4);
|
||||
start = apngWriter.position();
|
||||
apngWriter.writeFourCC(IDATChunk.ID);
|
||||
|
||||
reader.reset();
|
||||
// skip to fdat data position
|
||||
reader.skip(chunk.offset + 4 + 4 + 4);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length - 4);
|
||||
|
||||
apngWriter.skip(chunk.length - 4);
|
||||
crc32.reset();
|
||||
crc32.update(apngWriter.toByteArray(), start, chunk.length);
|
||||
apngWriter.writeInt((int) crc32.getValue());
|
||||
}
|
||||
}
|
||||
//endChunk
|
||||
apngWriter.putBytes(sPNGEndChunk);
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||
try {
|
||||
int length = encode(writer);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inMutable = true;
|
||||
options.inBitmap = reusedBitmap;
|
||||
byte[] bytes = writer.toByteArray();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options);
|
||||
assert bitmap != null;
|
||||
canvas.drawBitmap(bitmap, (float) frameX / sampleSize, (float) frameY / sampleSize, paint);
|
||||
return bitmap;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
143
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
143
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @link {https://www.w3.org/TR/PNG/#5PNG-file-signature}
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGParser {
|
||||
static class FormatException extends IOException {
|
||||
FormatException() {
|
||||
super("APNG Format error");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(String filePath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(filePath);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Context context, String assetPath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetPath);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Context context, int resId) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getResources().openRawResource(resId);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Reader in) {
|
||||
APNGReader reader = (in instanceof APNGReader) ? (APNGReader) in : new APNGReader(in);
|
||||
try {
|
||||
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||
throw new FormatException();
|
||||
}
|
||||
while (reader.available() > 0) {
|
||||
Chunk chunk = parseChunk(reader);
|
||||
if (chunk instanceof ACTLChunk) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<Chunk> parse(APNGReader reader) throws IOException {
|
||||
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
List<Chunk> chunks = new ArrayList<>();
|
||||
while (reader.available() > 0) {
|
||||
chunks.add(parseChunk(reader));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private static Chunk parseChunk(APNGReader reader) throws IOException {
|
||||
int offset = reader.position();
|
||||
int size = reader.readInt();
|
||||
int fourCC = reader.readFourCC();
|
||||
Chunk chunk;
|
||||
if (fourCC == ACTLChunk.ID) {
|
||||
chunk = new ACTLChunk();
|
||||
} else if (fourCC == FCTLChunk.ID) {
|
||||
chunk = new FCTLChunk();
|
||||
} else if (fourCC == FDATChunk.ID) {
|
||||
chunk = new FDATChunk();
|
||||
} else if (fourCC == IDATChunk.ID) {
|
||||
chunk = new IDATChunk();
|
||||
} else if (fourCC == IENDChunk.ID) {
|
||||
chunk = new IENDChunk();
|
||||
} else if (fourCC == IHDRChunk.ID) {
|
||||
chunk = new IHDRChunk();
|
||||
} else {
|
||||
chunk = new Chunk();
|
||||
}
|
||||
chunk.offset = offset;
|
||||
chunk.fourcc = fourCC;
|
||||
chunk.length = size;
|
||||
chunk.parse(reader);
|
||||
chunk.crc = reader.readInt();
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
|
||||
* Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
|
||||
* Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
|
||||
* CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码
|
||||
* @Link https://www.w3.org/TR/PNG
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class Chunk {
|
||||
int length;
|
||||
int fourcc;
|
||||
int crc;
|
||||
int offset;
|
||||
|
||||
static int fourCCToInt(String fourCC) {
|
||||
if (TextUtils.isEmpty(fourCC) || fourCC.length() != 4) {
|
||||
return 0xbadeffff;
|
||||
}
|
||||
return (fourCC.charAt(0) & 0xff)
|
||||
| (fourCC.charAt(1) & 0xff) << 8
|
||||
| (fourCC.charAt(2) & 0xff) << 16
|
||||
| (fourCC.charAt(3) & 0xff) << 24
|
||||
;
|
||||
}
|
||||
|
||||
void parse(APNGReader reader) throws IOException {
|
||||
int available = reader.available();
|
||||
innerParse(reader);
|
||||
int offset = available - reader.available();
|
||||
if (offset > length) {
|
||||
throw new IOException("Out of chunk area");
|
||||
} else if (offset < length) {
|
||||
reader.skip(length - offset);
|
||||
}
|
||||
}
|
||||
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
}
|
||||
}
|
||||
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
* @see {link=https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fcTL.27:_The_Frame_Control_Chunk}
|
||||
*/
|
||||
class FCTLChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("fcTL");
|
||||
int sequence_number;
|
||||
/**
|
||||
* x_offset >= 0
|
||||
* y_offset >= 0
|
||||
* width > 0
|
||||
* height > 0
|
||||
* x_offset + width <= 'IHDR' width
|
||||
* y_offset + height <= 'IHDR' height
|
||||
*/
|
||||
/**
|
||||
* Width of the following frame.
|
||||
*/
|
||||
int width;
|
||||
/**
|
||||
* Height of the following frame.
|
||||
*/
|
||||
int height;
|
||||
/**
|
||||
* X position at which to render the following frame.
|
||||
*/
|
||||
int x_offset;
|
||||
/**
|
||||
* Y position at which to render the following frame.
|
||||
*/
|
||||
int y_offset;
|
||||
|
||||
/**
|
||||
* The delay_num and delay_den parameters together specify a fraction indicating the time to
|
||||
* display the current frame, in seconds. If the denominator is 0, it is to be treated as if it
|
||||
* were 100 (that is, delay_num then specifies 1/100ths of a second).
|
||||
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as
|
||||
* possible, though viewers may impose a reasonable lower bound.
|
||||
* <p>
|
||||
* Frame timings should be independent of the time required for decoding and display of each frame,
|
||||
* so that animations will run at the same rate regardless of the performance of the decoder implementation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Frame delay fraction numerator.
|
||||
*/
|
||||
short delay_num;
|
||||
|
||||
/**
|
||||
* Frame delay fraction denominator.
|
||||
*/
|
||||
short delay_den;
|
||||
|
||||
/**
|
||||
* Type of frame area disposal to be done after rendering this frame.
|
||||
* dispose_op specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
|
||||
* If the first 'fcTL' chunk uses a dispose_op of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
|
||||
*/
|
||||
byte dispose_op;
|
||||
|
||||
/**
|
||||
* Type of frame area rendering for this frame.
|
||||
*/
|
||||
byte blend_op;
|
||||
|
||||
/**
|
||||
* No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_NON = 0;
|
||||
|
||||
/**
|
||||
* The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_BACKGROUND = 1;
|
||||
|
||||
/**
|
||||
* The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_PREVIOUS = 2;
|
||||
|
||||
/**
|
||||
* blend_op<code> specifies whether the frame is to be alpha blended into the current output buffer content,
|
||||
* or whether it should completely replace its region in the output buffer.
|
||||
*/
|
||||
/**
|
||||
* All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
|
||||
*/
|
||||
static final int APNG_BLEND_OP_SOURCE = 0;
|
||||
|
||||
/**
|
||||
* The frame should be composited onto the output buffer based on its alpha,
|
||||
* using a simple OVER operation as described in the Alpha Channel Processing section of the Extensions
|
||||
* to the PNG Specification, Version 1.2.0. Note that the second variation of the sample code is applicable.
|
||||
*/
|
||||
static final int APNG_BLEND_OP_OVER = 1;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
sequence_number = reader.readInt();
|
||||
width = reader.readInt();
|
||||
height = reader.readInt();
|
||||
x_offset = reader.readInt();
|
||||
y_offset = reader.readInt();
|
||||
delay_num = reader.readShort();
|
||||
delay_den = reader.readShort();
|
||||
dispose_op = reader.peek();
|
||||
blend_op = reader.peek();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fdAT.27:_The_Frame_Data_Chunk
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class FDATChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("fdAT");
|
||||
int sequence_number;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
sequence_number = reader.readInt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IDATChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("IDAT");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IENDChunk extends Chunk {
|
||||
static final int ID = Chunk.fourCCToInt("IEND");
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* The IHDR chunk shall be the first chunk in the PNG datastream. It contains:
|
||||
* <p>
|
||||
* Width 4 bytes
|
||||
* Height 4 bytes
|
||||
* Bit depth 1 byte
|
||||
* Colour type 1 byte
|
||||
* Compression method 1 byte
|
||||
* Filter method 1 byte
|
||||
* Interlace method 1 byte
|
||||
*
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IHDRChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("IHDR");
|
||||
/**
|
||||
* 图像宽度,以像素为单位
|
||||
*/
|
||||
int width;
|
||||
/**
|
||||
* 图像高度,以像素为单位
|
||||
*/
|
||||
int height;
|
||||
|
||||
byte[] data = new byte[5];
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
width = reader.readInt();
|
||||
height = reader.readInt();
|
||||
reader.read(data, 0, data.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class StillFrame extends Frame<APNGReader, APNGWriter> {
|
||||
|
||||
public StillFrame(APNGReader reader) {
|
||||
super(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inMutable = true;
|
||||
options.inBitmap = reusedBitmap;
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
reader.reset();
|
||||
bitmap = BitmapFactory.decodeStream(reader.toInputStream(), null, options);
|
||||
assert bitmap != null;
|
||||
paint.setXfermode(null);
|
||||
canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.common.io.FilterReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNGReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGReader extends FilterReader {
|
||||
private static ThreadLocal<byte[]> __intBytes = new ThreadLocal<>();
|
||||
|
||||
|
||||
protected static byte[] ensureBytes() {
|
||||
byte[] bytes = __intBytes.get();
|
||||
if (bytes == null) {
|
||||
bytes = new byte[4];
|
||||
__intBytes.set(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public APNGReader(Reader in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 4);
|
||||
return buf[3] & 0xFF |
|
||||
(buf[2] & 0xFF) << 8 |
|
||||
(buf[1] & 0xFF) << 16 |
|
||||
(buf[0] & 0xFF) << 24;
|
||||
}
|
||||
|
||||
public short readShort() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 2);
|
||||
return (short) (buf[1] & 0xFF |
|
||||
(buf[0] & 0xFF) << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return read FourCC and match chars
|
||||
*/
|
||||
public boolean matchFourCC(String chars) throws IOException {
|
||||
if (TextUtils.isEmpty(chars) || chars.length() != 4) {
|
||||
return false;
|
||||
}
|
||||
int fourCC = readFourCC();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (((fourCC >> (i * 8)) & 0xff) != chars.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int readFourCC() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 4);
|
||||
return buf[0] & 0xff | (buf[1] & 0xff) << 8 | (buf[2] & 0xff) << 16 | (buf[3] & 0xff) << 24;
|
||||
}
|
||||
}
|
||||
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferWriter;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* @Description: APNGWriter
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGWriter extends ByteBufferWriter {
|
||||
public APNGWriter() {
|
||||
super();
|
||||
}
|
||||
|
||||
public void writeFourCC(int val) {
|
||||
putByte((byte) (val & 0xff));
|
||||
putByte((byte) ((val >> 8) & 0xff));
|
||||
putByte((byte) ((val >> 16) & 0xff));
|
||||
putByte((byte) ((val >> 24) & 0xff));
|
||||
}
|
||||
|
||||
public void writeInt(int val) {
|
||||
putByte((byte) ((val >> 24) & 0xff));
|
||||
putByte((byte) ((val >> 16) & 0xff));
|
||||
putByte((byte) ((val >> 8) & 0xff));
|
||||
putByte((byte) (val & 0xff));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(int size) {
|
||||
super.reset(size);
|
||||
this.byteBuffer.order(ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.DrawFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PaintFlagsDrawFilter;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Description: Frame animation drawable
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameAnimationDrawable<Decoder extends FrameSeqDecoder> extends Drawable implements Animatable2Compat, FrameSeqDecoder.RenderListener {
|
||||
private static final String TAG = FrameAnimationDrawable.class.getSimpleName();
|
||||
private final Paint paint = new Paint();
|
||||
private final Decoder frameSeqDecoder;
|
||||
private DrawFilter drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
private Matrix matrix = new Matrix();
|
||||
private Set<AnimationCallback> animationCallbacks = new HashSet<>();
|
||||
private Bitmap bitmap;
|
||||
private static final int MSG_ANIMATION_START = 1;
|
||||
private static final int MSG_ANIMATION_END = 2;
|
||||
private Handler uiHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_ANIMATION_START:
|
||||
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||
animationCallback.onAnimationStart(FrameAnimationDrawable.this);
|
||||
}
|
||||
break;
|
||||
case MSG_ANIMATION_END:
|
||||
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||
animationCallback.onAnimationEnd(FrameAnimationDrawable.this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
private Runnable invalidateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
invalidateSelf();
|
||||
}
|
||||
};
|
||||
private boolean autoPlay = true;
|
||||
|
||||
public FrameAnimationDrawable(Decoder frameSeqDecoder) {
|
||||
paint.setAntiAlias(true);
|
||||
this.frameSeqDecoder = frameSeqDecoder;
|
||||
}
|
||||
|
||||
public FrameAnimationDrawable(Loader provider) {
|
||||
paint.setAntiAlias(true);
|
||||
this.frameSeqDecoder = createFrameSeqDecoder(provider, this);
|
||||
}
|
||||
|
||||
public void setAutoPlay(boolean autoPlay) {
|
||||
this.autoPlay = autoPlay;
|
||||
}
|
||||
|
||||
protected abstract Decoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener);
|
||||
|
||||
/**
|
||||
* @param loopLimit <=0为无限播放,>0为实际播放次数
|
||||
*/
|
||||
public void setLoopLimit(int loopLimit) {
|
||||
frameSeqDecoder.setLoopLimit(loopLimit);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
frameSeqDecoder.reset();
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
frameSeqDecoder.pause();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
frameSeqDecoder.resume();
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return frameSeqDecoder.isPaused();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (autoPlay) {
|
||||
frameSeqDecoder.start();
|
||||
} else {
|
||||
this.frameSeqDecoder.addRenderListener(this);
|
||||
if (!this.frameSeqDecoder.isRunning()) {
|
||||
this.frameSeqDecoder.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (autoPlay) {
|
||||
frameSeqDecoder.stop();
|
||||
} else {
|
||||
this.frameSeqDecoder.removeRenderListener(this);
|
||||
this.frameSeqDecoder.stopIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return frameSeqDecoder.isRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (bitmap == null || bitmap.isRecycled()) {
|
||||
return;
|
||||
}
|
||||
canvas.setDrawFilter(drawFilter);
|
||||
canvas.drawBitmap(bitmap, matrix, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBounds(int left, int top, int right, int bottom) {
|
||||
super.setBounds(left, top, right, bottom);
|
||||
boolean sampleSizeChanged = frameSeqDecoder.setDesiredSize(getBounds().width(), getBounds().height());
|
||||
matrix.setScale(
|
||||
1.0f * getBounds().width() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().width(),
|
||||
1.0f * getBounds().height() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().height());
|
||||
|
||||
if (sampleSizeChanged)
|
||||
this.bitmap = Bitmap.createBitmap(
|
||||
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
paint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
Message.obtain(uiHandler, MSG_ANIMATION_START).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRender(ByteBuffer byteBuffer) {
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
if (this.bitmap == null || this.bitmap.isRecycled()) {
|
||||
this.bitmap = Bitmap.createBitmap(
|
||||
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
byteBuffer.rewind();
|
||||
if (byteBuffer.remaining() < this.bitmap.getByteCount()) {
|
||||
Log.e(TAG, "onRender:Buffer not large enough for pixels");
|
||||
return;
|
||||
}
|
||||
this.bitmap.copyPixelsFromBuffer(byteBuffer);
|
||||
uiHandler.post(invalidateRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnd() {
|
||||
Message.obtain(uiHandler, MSG_ANIMATION_END).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
if (this.autoPlay) {
|
||||
if (visible) {
|
||||
if (!isRunning()) {
|
||||
start();
|
||||
}
|
||||
} else if (isRunning()) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
return super.setVisible(visible, restart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
try {
|
||||
return frameSeqDecoder.getBounds().width();
|
||||
} catch (Exception exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
try {
|
||||
return frameSeqDecoder.getBounds().height();
|
||||
} catch (Exception exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||
this.animationCallbacks.add(animationCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unregisterAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||
return this.animationCallbacks.remove(animationCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAnimationCallbacks() {
|
||||
this.animationCallbacks.clear();
|
||||
}
|
||||
}
|
||||
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
|
||||
/**
|
||||
* @Description: One frame in an animation
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public abstract class Frame<R extends Reader, W extends Writer> {
|
||||
protected final R reader;
|
||||
public int frameWidth;
|
||||
public int frameHeight;
|
||||
public int frameX;
|
||||
public int frameY;
|
||||
public int frameDuration;
|
||||
|
||||
public Frame(R reader) {
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer);
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.executor.FrameDecoderExecutor;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
/**
|
||||
* @Description: Abstract Frame Animation Decoder
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
|
||||
private static final String TAG = FrameSeqDecoder.class.getSimpleName();
|
||||
private final int taskId;
|
||||
|
||||
private final Loader mLoader;
|
||||
private final Handler workerHandler;
|
||||
protected List<Frame> frames = new ArrayList<>();
|
||||
protected int frameIndex = -1;
|
||||
private int playCount;
|
||||
private Integer loopLimit = null;
|
||||
private Set<RenderListener> renderListeners = new HashSet<>();
|
||||
private AtomicBoolean paused = new AtomicBoolean(true);
|
||||
private static final Rect RECT_EMPTY = new Rect();
|
||||
private Runnable renderTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (paused.get()) {
|
||||
return;
|
||||
}
|
||||
if (canStep()) {
|
||||
long start = System.currentTimeMillis();
|
||||
long delay = step();
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
workerHandler.postDelayed(this, Math.max(0, delay - cost));
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onRender(frameBuffer);
|
||||
}
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
protected int sampleSize = 1;
|
||||
|
||||
private Set<Bitmap> cacheBitmaps = new HashSet<>();
|
||||
protected Map<Bitmap, Canvas> cachedCanvas = new WeakHashMap<>();
|
||||
protected ByteBuffer frameBuffer;
|
||||
protected volatile Rect fullRect;
|
||||
private W mWriter = getWriter();
|
||||
private R mReader = null;
|
||||
|
||||
/**
|
||||
* If played all the needed
|
||||
*/
|
||||
private boolean finished = false;
|
||||
|
||||
private enum State {
|
||||
IDLE,
|
||||
RUNNING,
|
||||
INITIALIZING,
|
||||
FINISHING,
|
||||
}
|
||||
|
||||
private volatile State mState = State.IDLE;
|
||||
|
||||
public Loader getLoader() {
|
||||
return mLoader;
|
||||
}
|
||||
|
||||
protected abstract W getWriter();
|
||||
|
||||
protected abstract R getReader(Reader reader);
|
||||
|
||||
protected Bitmap obtainBitmap(int width, int height) {
|
||||
Bitmap ret = null;
|
||||
Iterator<Bitmap> iterator = cacheBitmaps.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
int reuseSize = width * height * 4;
|
||||
ret = iterator.next();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (ret != null && ret.getAllocationByteCount() >= reuseSize) {
|
||||
iterator.remove();
|
||||
if (ret.getWidth() != width || ret.getHeight() != height) {
|
||||
ret.reconfigure(width, height, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
ret.eraseColor(0);
|
||||
return ret;
|
||||
}
|
||||
} else {
|
||||
if (ret != null && ret.getByteCount() >= reuseSize) {
|
||||
if (ret.getWidth() == width && ret.getHeight() == height) {
|
||||
iterator.remove();
|
||||
ret.eraseColor(0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Bitmap.Config config = Bitmap.Config.ARGB_8888;
|
||||
ret = Bitmap.createBitmap(width, height, config);
|
||||
} catch (OutOfMemoryError e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected void recycleBitmap(Bitmap bitmap) {
|
||||
if (bitmap != null && !cacheBitmaps.contains(bitmap)) {
|
||||
cacheBitmaps.add(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码器的渲染回调
|
||||
*/
|
||||
public interface RenderListener {
|
||||
/**
|
||||
* 播放开始
|
||||
*/
|
||||
void onStart();
|
||||
|
||||
/**
|
||||
* 帧播放
|
||||
*/
|
||||
void onRender(ByteBuffer byteBuffer);
|
||||
|
||||
/**
|
||||
* 播放结束
|
||||
*/
|
||||
void onEnd();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param loader webp的reader
|
||||
* @param renderListener 渲染的回调
|
||||
*/
|
||||
public FrameSeqDecoder(Loader loader, @Nullable RenderListener renderListener) {
|
||||
this.mLoader = loader;
|
||||
if (renderListener != null) {
|
||||
this.renderListeners.add(renderListener);
|
||||
}
|
||||
this.taskId = FrameDecoderExecutor.getInstance().generateTaskId();
|
||||
this.workerHandler = new Handler(FrameDecoderExecutor.getInstance().getLooper(taskId));
|
||||
}
|
||||
|
||||
|
||||
public void addRenderListener(final RenderListener renderListener) {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderListeners.add(renderListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removeRenderListener(final RenderListener renderListener) {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderListeners.remove(renderListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopIfNeeded() {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (renderListeners.size() == 0) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Rect getBounds() {
|
||||
if (fullRect == null) {
|
||||
if (mState == State.FINISHING) {
|
||||
Log.e(TAG, "In finishing,do not interrupt");
|
||||
}
|
||||
final Thread thread = Thread.currentThread();
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (fullRect == null) {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
fullRect = RECT_EMPTY;
|
||||
} finally {
|
||||
LockSupport.unpark(thread);
|
||||
}
|
||||
}
|
||||
});
|
||||
LockSupport.park(thread);
|
||||
}
|
||||
return fullRect;
|
||||
}
|
||||
|
||||
private void initCanvasBounds(Rect rect) {
|
||||
fullRect = rect;
|
||||
frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4);
|
||||
if (mWriter == null) {
|
||||
mWriter = getWriter();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int getFrameCount() {
|
||||
return this.frames.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Loop Count defined in file
|
||||
*/
|
||||
protected abstract int getLoopCount();
|
||||
|
||||
public void start() {
|
||||
if (fullRect == RECT_EMPTY) {
|
||||
return;
|
||||
}
|
||||
if (mState == State.RUNNING || mState == State.INITIALIZING) {
|
||||
Log.i(TAG, debugInfo() + " Already started");
|
||||
return;
|
||||
}
|
||||
if (mState == State.FINISHING) {
|
||||
Log.e(TAG, debugInfo() + " Processing,wait for finish at " + mState);
|
||||
}
|
||||
mState = State.INITIALIZING;
|
||||
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||
innerStart();
|
||||
} else {
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void innerStart() {
|
||||
paused.compareAndSet(true, false);
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
try {
|
||||
if (frames.size() == 0) {
|
||||
try {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Log.i(TAG, debugInfo() + " Set state to RUNNING,cost " + (System.currentTimeMillis() - start));
|
||||
mState = State.RUNNING;
|
||||
}
|
||||
if (getNumPlays() == 0 || !finished) {
|
||||
this.frameIndex = -1;
|
||||
renderTask.run();
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onStart();
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, debugInfo() + " No need to started");
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void innerStop() {
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
frames.clear();
|
||||
for (Bitmap bitmap : cacheBitmaps) {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
cacheBitmaps.clear();
|
||||
if (frameBuffer != null) {
|
||||
frameBuffer = null;
|
||||
}
|
||||
cachedCanvas.clear();
|
||||
try {
|
||||
if (mReader != null) {
|
||||
mReader.close();
|
||||
mReader = null;
|
||||
}
|
||||
if (mWriter != null) {
|
||||
mWriter.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
release();
|
||||
mState = State.IDLE;
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (fullRect == RECT_EMPTY) {
|
||||
return;
|
||||
}
|
||||
if (mState == State.FINISHING || mState == State.IDLE) {
|
||||
Log.i(TAG, debugInfo() + "No need to stop");
|
||||
return;
|
||||
}
|
||||
if (mState == State.INITIALIZING) {
|
||||
Log.e(TAG, debugInfo() + "Processing,wait for finish at " + mState);
|
||||
}
|
||||
mState = State.FINISHING;
|
||||
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||
innerStop();
|
||||
} else {
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String debugInfo() {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected abstract void release();
|
||||
|
||||
public boolean isRunning() {
|
||||
return mState == State.RUNNING || mState == State.INITIALIZING;
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return paused.get();
|
||||
}
|
||||
|
||||
public void setLoopLimit(int limit) {
|
||||
this.loopLimit = limit;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.playCount = 0;
|
||||
this.frameIndex = -1;
|
||||
this.finished = false;
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
paused.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused.compareAndSet(true, false);
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
workerHandler.post(renderTask);
|
||||
}
|
||||
|
||||
|
||||
public int getSampleSize() {
|
||||
return sampleSize;
|
||||
}
|
||||
|
||||
public boolean setDesiredSize(int width, int height) {
|
||||
boolean sampleSizeChanged = false;
|
||||
int sample = getDesiredSample(width, height);
|
||||
if (sample != this.sampleSize) {
|
||||
this.sampleSize = sample;
|
||||
sampleSizeChanged = true;
|
||||
final boolean tempRunning = isRunning();
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStop();
|
||||
try {
|
||||
initCanvasBounds(read(getReader(mLoader.obtain())));
|
||||
if (tempRunning) {
|
||||
innerStart();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return sampleSizeChanged;
|
||||
}
|
||||
|
||||
protected int getDesiredSample(int desiredWidth, int desiredHeight) {
|
||||
if (desiredWidth == 0 || desiredHeight == 0) {
|
||||
return 1;
|
||||
}
|
||||
int radio = Math.min(getBounds().width() / desiredWidth, getBounds().height() / desiredHeight);
|
||||
int sample = 1;
|
||||
while ((sample * 2) <= radio) {
|
||||
sample *= 2;
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
|
||||
protected abstract Rect read(R reader) throws IOException;
|
||||
|
||||
private int getNumPlays() {
|
||||
return this.loopLimit != null ? this.loopLimit : this.getLoopCount();
|
||||
}
|
||||
|
||||
private boolean canStep() {
|
||||
if (!isRunning()) {
|
||||
return false;
|
||||
}
|
||||
if (frames.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
if (getNumPlays() <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (this.playCount < getNumPlays() - 1) {
|
||||
return true;
|
||||
} else if (this.playCount == getNumPlays() - 1 && this.frameIndex < this.getFrameCount() - 1) {
|
||||
return true;
|
||||
}
|
||||
finished = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private long step() {
|
||||
this.frameIndex++;
|
||||
if (this.frameIndex >= this.getFrameCount()) {
|
||||
this.frameIndex = 0;
|
||||
this.playCount++;
|
||||
}
|
||||
Frame frame = getFrame(this.frameIndex);
|
||||
if (frame == null) {
|
||||
return 0;
|
||||
}
|
||||
renderFrame(frame);
|
||||
return frame.frameDuration;
|
||||
}
|
||||
|
||||
protected abstract void renderFrame(Frame frame);
|
||||
|
||||
private Frame getFrame(int index) {
|
||||
if (index < 0 || index >= frames.size()) {
|
||||
return null;
|
||||
}
|
||||
return frames.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Indexed frame
|
||||
*
|
||||
* @param index <0 means reverse from last index
|
||||
*/
|
||||
public Bitmap getFrameBitmap(int index) throws IOException {
|
||||
if (mState != State.IDLE) {
|
||||
Log.e(TAG, debugInfo() + ",stop first");
|
||||
return null;
|
||||
}
|
||||
mState = State.RUNNING;
|
||||
paused.compareAndSet(true, false);
|
||||
if (frames.size() == 0) {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
}
|
||||
if (index < 0) {
|
||||
index += this.frames.size();
|
||||
}
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
frameIndex = -1;
|
||||
while (frameIndex < index) {
|
||||
if (canStep()) {
|
||||
step();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
frameBuffer.rewind();
|
||||
Bitmap bitmap = Bitmap.createBitmap(getBounds().width() / getSampleSize(), getBounds().height() / getSampleSize(), Bitmap.Config.ARGB_8888);
|
||||
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||
innerStop();
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.executor;
|
||||
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @Description: com.github.penfeizhou.animation.executor
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-11-21
|
||||
*/
|
||||
public class FrameDecoderExecutor {
|
||||
private static int sPoolNumber = 4;
|
||||
private ArrayList<HandlerThread> mHandlerThreadGroup = new ArrayList<>();
|
||||
private AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private FrameDecoderExecutor() {
|
||||
}
|
||||
|
||||
static class Inner {
|
||||
static final FrameDecoderExecutor sInstance = new FrameDecoderExecutor();
|
||||
}
|
||||
|
||||
public void setPoolSize(int size) {
|
||||
sPoolNumber = size;
|
||||
}
|
||||
|
||||
public static FrameDecoderExecutor getInstance() {
|
||||
return Inner.sInstance;
|
||||
}
|
||||
|
||||
public Looper getLooper(int taskId) {
|
||||
int idx = taskId % sPoolNumber;
|
||||
if (idx >= mHandlerThreadGroup.size()) {
|
||||
HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx);
|
||||
handlerThread.start();
|
||||
|
||||
mHandlerThreadGroup.add(handlerThread);
|
||||
Looper looper = handlerThread.getLooper();
|
||||
if (looper != null) {
|
||||
return looper;
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
} else {
|
||||
if (mHandlerThreadGroup.get(idx) != null) {
|
||||
Looper looper = mHandlerThreadGroup.get(idx).getLooper();
|
||||
if (looper != null) {
|
||||
return looper;
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int generateTaskId() {
|
||||
return counter.getAndIncrement();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-14
|
||||
*/
|
||||
public class ByteBufferReader implements Reader {
|
||||
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public ByteBufferReader(ByteBuffer byteBuffer) {
|
||||
this.byteBuffer = byteBuffer;
|
||||
byteBuffer.position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long total) throws IOException {
|
||||
byteBuffer.position((int) (byteBuffer.position() + total));
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
return byteBuffer.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
byteBuffer.position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||
byteBuffer.get(buffer, start, byteCount);
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return byteBuffer.limit() - byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(byteBuffer.array());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* @Description: ByteBufferWriter
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-12
|
||||
*/
|
||||
public class ByteBufferWriter implements Writer {
|
||||
|
||||
protected ByteBuffer byteBuffer;
|
||||
|
||||
public ByteBufferWriter() {
|
||||
reset(10 * 1024);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putByte(byte b) {
|
||||
byteBuffer.put(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putBytes(byte[] b) {
|
||||
byteBuffer.put(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skip(int length) {
|
||||
byteBuffer.position(length + position());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(int size) {
|
||||
if (byteBuffer == null || size > byteBuffer.capacity()) {
|
||||
byteBuffer = ByteBuffer.allocate(size);
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
byteBuffer.clear();
|
||||
}
|
||||
}
|
||||
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: FileReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-23
|
||||
*/
|
||||
public class FileReader extends FilterReader {
|
||||
private final File mFile;
|
||||
|
||||
public FileReader(File file) throws IOException {
|
||||
super(new StreamReader(new FileInputStream(file)));
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
reader.close();
|
||||
reader = new StreamReader(new FileInputStream(mFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: FilterReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-23
|
||||
*/
|
||||
public class FilterReader implements Reader {
|
||||
protected Reader reader;
|
||||
|
||||
public FilterReader(Reader in) {
|
||||
this.reader = in;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long total) throws IOException {
|
||||
return reader.skip(total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
return reader.peek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
reader.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return reader.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||
return reader.read(buffer, start, byteCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return reader.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
reader.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
reset();
|
||||
return reader.toInputStream();
|
||||
}
|
||||
}
|
||||
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @link {https://developers.google.com/speed/webp/docs/riff_container#terminology_basics}
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-11
|
||||
*/
|
||||
public interface Reader {
|
||||
long skip(long total) throws IOException;
|
||||
|
||||
byte peek() throws IOException;
|
||||
|
||||
void reset() throws IOException;
|
||||
|
||||
int position();
|
||||
|
||||
int read(byte[] buffer, int start, int byteCount) throws IOException;
|
||||
|
||||
int available() throws IOException;
|
||||
|
||||
/**
|
||||
* close io
|
||||
*/
|
||||
void close() throws IOException;
|
||||
|
||||
InputStream toInputStream() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-11
|
||||
*/
|
||||
public class StreamReader extends FilterInputStream implements Reader {
|
||||
private int position;
|
||||
|
||||
public StreamReader(InputStream in) {
|
||||
super(in);
|
||||
try {
|
||||
in.reset();
|
||||
} catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
byte ret = (byte) read();
|
||||
position++;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int ret = super.read(b, off, len);
|
||||
position += Math.max(0, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
super.reset();
|
||||
position = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long ret = super.skip(n);
|
||||
position += ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-12
|
||||
*/
|
||||
public interface Writer {
|
||||
void reset(int size);
|
||||
|
||||
void putByte(byte b);
|
||||
|
||||
void putBytes(byte[] b);
|
||||
|
||||
int position();
|
||||
|
||||
void skip(int length);
|
||||
|
||||
byte[] toByteArray();
|
||||
|
||||
void close() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: 从Asset中读取流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class AssetStreamLoader extends StreamLoader {
|
||||
|
||||
private final Context mContext;
|
||||
private final String mAssetName;
|
||||
|
||||
public AssetStreamLoader(Context context, String assetName) {
|
||||
mContext = context.getApplicationContext();
|
||||
mAssetName = assetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return mContext.getAssets().open(mAssetName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @Description: ByteBufferLoader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-15
|
||||
*/
|
||||
public abstract class ByteBufferLoader implements Loader {
|
||||
public abstract ByteBuffer getByteBuffer();
|
||||
|
||||
@Override
|
||||
public Reader obtain() throws IOException {
|
||||
return new ByteBufferReader(getByteBuffer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.FileReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: 从文件加载流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class FileLoader implements Loader {
|
||||
|
||||
private final File mFile;
|
||||
private Reader mReader;
|
||||
|
||||
public FileLoader(String path) {
|
||||
mFile = new File(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Reader obtain() throws IOException {
|
||||
return new FileReader(mFile);
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: Loader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-14
|
||||
*/
|
||||
public interface Loader {
|
||||
Reader obtain() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: 从资源加载流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class ResourceStreamLoader extends StreamLoader {
|
||||
private final Context mContext;
|
||||
private final int mResId;
|
||||
|
||||
|
||||
public ResourceStreamLoader(Context context, int resId) {
|
||||
mContext = context.getApplicationContext();
|
||||
mResId = resId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return mContext.getResources().openRawResource(mResId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public abstract class StreamLoader implements Loader {
|
||||
protected abstract InputStream getInputStream() throws IOException;
|
||||
|
||||
|
||||
public final synchronized Reader obtain() throws IOException {
|
||||
return new StreamReader(getInputStream());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -32,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
@@ -46,6 +48,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -68,6 +71,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
@@ -127,12 +131,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
initializePendingMessages();
|
||||
initializeBlobProvider();
|
||||
initializeCleanup();
|
||||
initializeGlideCodecs();
|
||||
|
||||
FeatureFlags.init();
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNeccessary(this);
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
RegistrationUtil.maybeMarkRegistrationComplete(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
@@ -154,6 +159,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
checkBuildExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,6 +195,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
return persistentLogger;
|
||||
}
|
||||
|
||||
public void checkBuildExpiration() {
|
||||
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build expired!");
|
||||
SignalStore.misc().markClientDeprecated();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSecurityProvider() {
|
||||
try {
|
||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||
@@ -378,6 +391,35 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeGlideCodecs() {
|
||||
SignalGlideCodecs.setLogProvider(new org.signal.glide.Log.Provider() {
|
||||
@Override
|
||||
public void v(@NonNull String tag, @NonNull String message) {
|
||||
Log.v(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void d(@NonNull String tag, @NonNull String message) {
|
||||
Log.d(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void i(@NonNull String tag, @NonNull String message) {
|
||||
Log.i(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void w(@NonNull String tag, @NonNull String message) {
|
||||
Log.w(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
Log.e(tag, message, throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -30,6 +33,7 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.help.HelpFragment;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||
@@ -39,10 +43,14 @@ import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
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;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
@@ -60,6 +68,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
|
||||
|
||||
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";
|
||||
@@ -69,6 +78,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
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 final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
@@ -134,6 +144,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
public void pushFragment(@NonNull Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(android.R.id.content, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
|
||||
@Override
|
||||
@@ -142,6 +160,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
|
||||
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)
|
||||
@@ -159,7 +179,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DONATE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
|
||||
|
||||
tintIcons();
|
||||
}
|
||||
@@ -174,6 +196,24 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
@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
|
||||
@@ -188,6 +228,11 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
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)
|
||||
@@ -207,6 +252,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldDisplayUsernameReminder() {
|
||||
return FeatureFlags.usernames() && !Recipient.self().getUsername().isPresent() && SignalStore.misc().shouldShowUsernameReminder();
|
||||
}
|
||||
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
||||
private String category;
|
||||
|
||||
@@ -247,6 +296,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
case PREFERENCE_CATEGORY_HELP:
|
||||
fragment = new HelpFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DONATE:
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
@@ -255,14 +307,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||
|
||||
fragmentTransaction.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end);
|
||||
|
||||
fragmentTransaction.replace(android.R.id.content, fragment);
|
||||
fragmentTransaction.addToBackStack(null);
|
||||
fragmentTransaction.commit();
|
||||
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -276,6 +321,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -9,7 +10,10 @@ import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.transition.TransitionInflater;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
@@ -72,15 +76,19 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
|
||||
}
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
toolbar.getViewTreeObserver().addOnGlobalLayoutListener(new DisplayCutoutAdjuster(toolbar, findViewById(R.id.toolbar_cutout_spacer)));
|
||||
}
|
||||
|
||||
showSystemUI();
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
@@ -180,4 +188,36 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust a spacer for the toolbar when a display cutout is detected. Runs within
|
||||
* a layout listener because the activity delays view attachment due to the transitions
|
||||
* and needs to update on device rotation.
|
||||
*/
|
||||
@TargetApi(28)
|
||||
private static class DisplayCutoutAdjuster implements ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
private final View view;
|
||||
private final View spacer;
|
||||
|
||||
private DisplayCutoutAdjuster(@NonNull View view, @NonNull View spacer) {
|
||||
this.view = view;
|
||||
this.spacer = spacer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
if (view.getRootWindowInsets() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DisplayCutout cutout = view.getRootWindowInsets().getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
ViewGroup.LayoutParams params = spacer.getLayoutParams();
|
||||
params.height = cutout.getSafeInsetTop();
|
||||
spacer.setLayoutParams(params);
|
||||
spacer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -19,6 +20,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
|
||||
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
|
||||
@@ -72,9 +75,9 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
@TargetApi(21)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
@@ -87,4 +90,8 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
|
||||
public final @NonNull ActionBar requireSupportActionBar() {
|
||||
return Objects.requireNonNull(getSupportActionBar());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -21,17 +22,17 @@ import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable {
|
||||
void bind(@NonNull MessageRecord messageRecord,
|
||||
void bind(@NonNull ConversationMessage messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseHighlight);
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<ConversationMessage> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseMention);
|
||||
|
||||
MessageRecord getMessageRecord();
|
||||
ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
@@ -45,8 +46,11 @@ public interface BindableConversationItem extends Unbindable {
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
@@ -113,8 +114,8 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
}
|
||||
|
||||
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
@@ -137,8 +138,8 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
private void processIncomingMessageRecord(MessageRecord messageRecord) {
|
||||
try {
|
||||
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
||||
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
|
||||
@@ -113,7 +113,9 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {}
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
||||
|
||||
@@ -70,7 +70,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
@@ -473,11 +472,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
|
||||
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
@@ -488,11 +491,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
|
||||
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -506,7 +512,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
private boolean selectionLimitReached() {
|
||||
return getChipCount() >= selectionLimit;
|
||||
return getChipCount() + currentSelection.size() >= selectionLimit;
|
||||
}
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||
@@ -624,7 +630,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
void onContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
/** @return True if the contact is allowed to be selected, otherwise false. */
|
||||
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
@@ -103,7 +102,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
|
||||
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
@@ -121,13 +120,14 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
}
|
||||
|
||||
private void sendSmsInvites() {
|
||||
@@ -137,12 +137,11 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
.toArray(new SelectedContact[0]));
|
||||
}
|
||||
|
||||
private void updateSmsButtonText() {
|
||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
private void updateSmsButtonText(int count) {
|
||||
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
|
||||
selectedContacts.size(),
|
||||
selectedContacts.size()));
|
||||
smsSendButton.setEnabled(!selectedContacts.isEmpty());
|
||||
count,
|
||||
count));
|
||||
smsSendButton.setEnabled(count > 0);
|
||||
}
|
||||
|
||||
@Override public void onBackPressed() {
|
||||
@@ -156,7 +155,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
private void cancelSmsSelection() {
|
||||
setPrimaryColorsToolbarNormal();
|
||||
contactsFragment.reset();
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
@@ -18,6 +21,14 @@ public class MainActivity extends PassphraseRequiredActivity {
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
navigator.onCreate(savedInstanceState);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleGroupLinkInIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -42,4 +53,11 @@ public class MainActivity extends PassphraseRequiredActivity {
|
||||
public @NonNull MainNavigator getNavigator() {
|
||||
return navigator;
|
||||
}
|
||||
|
||||
private void handleGroupLinkInIntent(Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,13 +431,17 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.clear();
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
inflater.inflate(R.menu.media_preview, menu);
|
||||
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
if (!isMediaInDb()) {
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
menu.findItem(R.id.delete).setVisible(false);
|
||||
@@ -447,6 +451,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
}
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
@@ -92,6 +92,8 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
launch(Recipient.external(this, number));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
@@ -136,11 +138,11 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.clear();
|
||||
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,12 +132,13 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
inflater.inflate(R.menu.log_submit, menu);
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
byte[] localId;
|
||||
byte[] remoteId;
|
||||
|
||||
if (FeatureFlags.cds() && recipient.resolve().getUuid().isPresent()) {
|
||||
if (FeatureFlags.verifyV2() && recipient.resolve().getUuid().isPresent()) {
|
||||
Log.i(TAG, "Using UUID (version 2).");
|
||||
version = 2;
|
||||
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
|
||||
|
||||
@@ -27,9 +27,6 @@ import android.content.res.Configuration;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Rational;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
@@ -37,7 +34,6 @@ import android.view.WindowManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
@@ -48,7 +44,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||
@@ -57,23 +53,17 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class WebRtcCallActivity extends AppCompatActivity {
|
||||
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
|
||||
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
|
||||
|
||||
private static final int STANDARD_DELAY_FINISH = 1000;
|
||||
public static final int BUSY_SIGNAL_DELAY_FINISH = 5500;
|
||||
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||
@@ -417,8 +407,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
||||
|
||||
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
|
||||
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
|
||||
}
|
||||
|
||||
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
||||
@@ -470,37 +459,24 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
String name = recipient.getDisplayName(this);
|
||||
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
|
||||
SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
|
||||
}
|
||||
|
||||
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange() {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()));
|
||||
|
||||
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this);
|
||||
untrustedIdentityExplanation.setText(spannableString);
|
||||
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setView(untrustedIdentityExplanation)
|
||||
.setPositiveButton(R.string.WebRtcCallScreen_accept, (d, w) -> {
|
||||
synchronized (SESSION_LOCK) {
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
|
||||
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirKey, true);
|
||||
}
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() { }
|
||||
|
||||
d.dismiss();
|
||||
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
|
||||
|
||||
startService(intent);
|
||||
})
|
||||
.setNegativeButton(R.string.WebRtcCallScreen_end_call, (d, w) -> {
|
||||
d.dismiss();
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
})
|
||||
.show();
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
private boolean isSystemPipEnabledAndAvailable() {
|
||||
|
||||
@@ -156,7 +156,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processSticker(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Sticker sticker, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File stickerDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File stickerDirectory = context.getDir(StickerDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("sticker", ".mms", stickerDirectory);
|
||||
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheet
|
||||
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -193,6 +194,23 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
public void setImageBytesForGroup(@Nullable byte[] avatarBytes,
|
||||
@Nullable Recipient.FallbackPhotoProvider fallbackPhotoProvider,
|
||||
@NonNull MaterialColor color)
|
||||
{
|
||||
Drawable fallback = Util.firstNonNull(fallbackPhotoProvider, Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER)
|
||||
.getPhotoForGroup()
|
||||
.asDrawable(getContext(), color.toAvatarColor(getContext()));
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(avatarBytes)
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.circleCrop()
|
||||
.into(this);
|
||||
}
|
||||
|
||||
private static class RecipientContactPhoto {
|
||||
|
||||
private final @NonNull Recipient recipient;
|
||||
|
||||
@@ -2,40 +2,57 @@ package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
import androidx.core.os.BuildCompat;
|
||||
|
||||
import android.text.Annotation;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextUtils.TruncateAt;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.util.AttributeSet;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionDeleter;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
|
||||
|
||||
public class ComposeText extends EmojiEditText {
|
||||
|
||||
private CharSequence hint;
|
||||
private SpannableString subHint;
|
||||
private CharSequence combinedHint;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private MentionValidatorWatcher mentionValidatorWatcher;
|
||||
|
||||
@Nullable private InputPanel.MediaListener mediaListener;
|
||||
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
||||
@Nullable private MentionQueryChangedListener mentionQueryChangedListener;
|
||||
|
||||
public ComposeText(Context context) {
|
||||
super(context);
|
||||
@@ -52,34 +69,72 @@ public class ComposeText extends EmojiEditText {
|
||||
initialize();
|
||||
}
|
||||
|
||||
public String getTextTrimmed(){
|
||||
return getText().toString().trim();
|
||||
/**
|
||||
* Trims and returns text while preserving potential spans like {@link MentionAnnotation}.
|
||||
*/
|
||||
public @NonNull CharSequence getTextTrimmed() {
|
||||
Editable text = getText();
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
return StringUtil.trimSequence(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
|
||||
if (!TextUtils.isEmpty(hint)) {
|
||||
if (!TextUtils.isEmpty(subHint)) {
|
||||
setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
|
||||
.append("\n")
|
||||
.append(ellipsizeToWidth(subHint)));
|
||||
} else {
|
||||
setHint(ellipsizeToWidth(hint));
|
||||
}
|
||||
if (!TextUtils.isEmpty(combinedHint)) {
|
||||
setHint(combinedHint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSelectionChanged(int selStart, int selEnd) {
|
||||
super.onSelectionChanged(selStart, selEnd);
|
||||
protected void onSelectionChanged(int selectionStart, int selectionEnd) {
|
||||
super.onSelectionChanged(selectionStart, selectionEnd);
|
||||
|
||||
if (FeatureFlags.mentions() && getText() != null) {
|
||||
boolean selectionChanged = changeSelectionForPartialMentions(getText(), selectionStart, selectionEnd);
|
||||
if (selectionChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectionStart == selectionEnd) {
|
||||
doAfterCursorChange(getText());
|
||||
} else {
|
||||
updateQuery(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorPositionChangedListener != null) {
|
||||
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
|
||||
cursorPositionChangedListener.onCursorPositionChanged(selectionStart, selectionEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (getText() != null && getLayout() != null) {
|
||||
int checkpoint = canvas.save();
|
||||
|
||||
// Clip using same logic as TextView drawing
|
||||
int maxScrollY = getLayout().getHeight() - getBottom() - getTop() - getCompoundPaddingBottom() - getCompoundPaddingTop();
|
||||
float clipLeft = getCompoundPaddingLeft() + getScrollX();
|
||||
float clipTop = (getScrollY() == 0) ? 0 : getExtendedPaddingTop() + getScrollY();
|
||||
float clipRight = getRight() - getLeft() - getCompoundPaddingRight() + getScrollX();
|
||||
float clipBottom = getBottom() - getTop() + getScrollY() - ((getScrollY() == maxScrollY) ? 0 : getExtendedPaddingBottom());
|
||||
|
||||
canvas.clipRect(clipLeft - 10, clipTop, clipRight + 10, clipBottom);
|
||||
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
|
||||
|
||||
try {
|
||||
mentionRendererDelegate.draw(canvas, getText(), getLayout());
|
||||
} finally {
|
||||
canvas.restoreToCount(checkpoint);
|
||||
}
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
private CharSequence ellipsizeToWidth(CharSequence text) {
|
||||
return TextUtils.ellipsize(text,
|
||||
getPaint(),
|
||||
@@ -88,25 +143,25 @@ public class ComposeText extends EmojiEditText {
|
||||
}
|
||||
|
||||
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
|
||||
this.hint = hint;
|
||||
|
||||
if (subHint != null) {
|
||||
this.subHint = new SpannableString(subHint);
|
||||
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
Spannable subHintSpannable = new SpannableString(subHint);
|
||||
subHintSpannable.setSpan(new RelativeSizeSpan(0.5f), 0, subHintSpannable.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
|
||||
combinedHint = new SpannableStringBuilder().append(ellipsizeToWidth(hint))
|
||||
.append("\n")
|
||||
.append(ellipsizeToWidth(subHintSpannable));
|
||||
} else {
|
||||
this.subHint = null;
|
||||
combinedHint = ellipsizeToWidth(hint);
|
||||
}
|
||||
|
||||
if (this.subHint != null) {
|
||||
super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint))
|
||||
.append("\n")
|
||||
.append(ellipsizeToWidth(this.subHint)));
|
||||
} else {
|
||||
super.setHint(ellipsizeToWidth(this.hint));
|
||||
}
|
||||
super.setHint(combinedHint);
|
||||
}
|
||||
|
||||
public void appendInvite(String invite) {
|
||||
if (getText() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(getText()) && !getText().toString().equals(" ")) {
|
||||
append(" ");
|
||||
}
|
||||
@@ -119,13 +174,22 @@ public class ComposeText extends EmojiEditText {
|
||||
this.cursorPositionChangedListener = listener;
|
||||
}
|
||||
|
||||
public void setMentionQueryChangedListener(@Nullable MentionQueryChangedListener listener) {
|
||||
this.mentionQueryChangedListener = listener;
|
||||
}
|
||||
|
||||
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
|
||||
if (FeatureFlags.mentions()) {
|
||||
mentionValidatorWatcher.setMentionValidator(mentionValidator);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLandscape() {
|
||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
public void setTransport(TransportOption transport) {
|
||||
final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext());
|
||||
final boolean isIncognito = TextSecurePreferences.isIncognitoKeyboardEnabled(getContext());
|
||||
|
||||
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
|
||||
int inputType = getInputType();
|
||||
@@ -137,12 +201,12 @@ public class ComposeText extends EmojiEditText {
|
||||
inputType = (inputType & ~InputType.TYPE_MASK_VARIATION) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE;
|
||||
}
|
||||
|
||||
setInputType(inputType);
|
||||
setImeOptions(imeOptions);
|
||||
setHint(transport.getComposeHint(),
|
||||
transport.getSimName().isPresent()
|
||||
? getContext().getString(R.string.conversation_activity__from_sim_name, transport.getSimName().get())
|
||||
: null);
|
||||
setInputType(inputType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -165,13 +229,131 @@ public class ComposeText extends EmojiEditText {
|
||||
this.mediaListener = mediaListener;
|
||||
}
|
||||
|
||||
public boolean hasMentions() {
|
||||
Editable text = getText();
|
||||
if (text != null) {
|
||||
return !MentionAnnotation.getMentionAnnotations(text).isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return MentionAnnotation.getMentionsFromAnnotations(getText());
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
|
||||
setImeOptions(getImeOptions() | 16777216);
|
||||
}
|
||||
|
||||
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ThemeUtil.getThemedColor(getContext(), R.attr.conversation_mention_background_color));
|
||||
|
||||
if (FeatureFlags.mentions()) {
|
||||
addTextChangedListener(new MentionDeleter());
|
||||
mentionValidatorWatcher = new MentionValidatorWatcher();
|
||||
addTextChangedListener(mentionValidatorWatcher);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean changeSelectionForPartialMentions(@NonNull Spanned spanned, int selectionStart, int selectionEnd) {
|
||||
Annotation[] annotations = spanned.getSpans(0, spanned.length(), Annotation.class);
|
||||
for (Annotation annotation : annotations) {
|
||||
if (MentionAnnotation.isMentionAnnotation(annotation)) {
|
||||
int spanStart = spanned.getSpanStart(annotation);
|
||||
int spanEnd = spanned.getSpanEnd(annotation);
|
||||
|
||||
boolean startInMention = selectionStart > spanStart && selectionStart < spanEnd;
|
||||
boolean endInMention = selectionEnd > spanStart && selectionEnd < spanEnd;
|
||||
|
||||
if (startInMention || endInMention) {
|
||||
if (selectionStart == selectionEnd) {
|
||||
setSelection(spanEnd, spanEnd);
|
||||
} else {
|
||||
int newStart = startInMention ? spanStart : selectionStart;
|
||||
int newEnd = endInMention ? spanEnd : selectionEnd;
|
||||
setSelection(newStart, newEnd);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void doAfterCursorChange(@NonNull Editable text) {
|
||||
if (enoughToFilter(text)) {
|
||||
performFiltering(text);
|
||||
} else {
|
||||
updateQuery(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void performFiltering(@NonNull Editable text) {
|
||||
int end = getSelectionEnd();
|
||||
int start = findQueryStart(text, end);
|
||||
CharSequence query = text.subSequence(start, end);
|
||||
updateQuery(query.toString());
|
||||
}
|
||||
|
||||
private void updateQuery(@Nullable String query) {
|
||||
if (mentionQueryChangedListener != null) {
|
||||
mentionQueryChangedListener.onQueryChanged(query);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean enoughToFilter(@NonNull Editable text) {
|
||||
int end = getSelectionEnd();
|
||||
if (end < 0) {
|
||||
return false;
|
||||
}
|
||||
return findQueryStart(text, end) != -1;
|
||||
}
|
||||
|
||||
public void replaceTextWithMention(@NonNull String displayName, @NonNull RecipientId recipientId) {
|
||||
Editable text = getText();
|
||||
if (text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearComposingText();
|
||||
|
||||
int end = getSelectionEnd();
|
||||
int start = findQueryStart(text, end) - 1;
|
||||
|
||||
text.replace(start, end, createReplacementToken(displayName, recipientId));
|
||||
}
|
||||
|
||||
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull RecipientId recipientId) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder().append(MENTION_STARTER);
|
||||
if (text instanceof Spanned) {
|
||||
SpannableString spannableString = new SpannableString(text + " ");
|
||||
TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, spannableString, 0);
|
||||
builder.append(spannableString);
|
||||
} else {
|
||||
builder.append(text).append(" ");
|
||||
}
|
||||
|
||||
builder.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(recipientId), 0, builder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private int findQueryStart(@NonNull CharSequence text, int inputCursorPosition) {
|
||||
if (inputCursorPosition == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int delimiterSearchIndex = inputCursorPosition - 1;
|
||||
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != MENTION_STARTER && text.charAt(delimiterSearchIndex) != ' ')) {
|
||||
delimiterSearchIndex--;
|
||||
}
|
||||
|
||||
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == MENTION_STARTER) {
|
||||
return delimiterSearchIndex + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2)
|
||||
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
||||
|
||||
private static final String TAG = CommitContentListener.class.getSimpleName();
|
||||
@@ -184,7 +366,7 @@ public class ComposeText extends EmojiEditText {
|
||||
|
||||
@Override
|
||||
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
|
||||
if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
|
||||
if (Build.VERSION.SDK_INT >= 25 && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
|
||||
try {
|
||||
inputContentInfo.requestPermission();
|
||||
} catch (Exception e) {
|
||||
@@ -207,4 +389,8 @@ public class ComposeText extends EmojiEditText {
|
||||
public interface CursorPositionChangedListener {
|
||||
void onCursorPositionChanged(int start, int end);
|
||||
}
|
||||
|
||||
public interface MentionQueryChangedListener {
|
||||
void onQueryChanged(@Nullable String query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
private ConversationItemFooter footer;
|
||||
private CornerMask cornerMask;
|
||||
private Outliner outliner;
|
||||
private Outliner pulseOutliner;
|
||||
private boolean borderless;
|
||||
|
||||
public ConversationItemThumbnail(Context context) {
|
||||
@@ -80,6 +81,14 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
outliner.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
if (pulseOutliner != null) {
|
||||
pulseOutliner.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPulseOutliner(@NonNull Outliner outliner) {
|
||||
this.pulseOutliner = outliner;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,6 +119,10 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
}
|
||||
|
||||
public void setMinimumThumbnailWidth(int width) {
|
||||
thumbnail.setMinimumThumbnailWidth(width);
|
||||
}
|
||||
|
||||
public void setBorderless(boolean borderless) {
|
||||
this.borderless = borderless;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class ConversationScrollToView extends FrameLayout {
|
||||
|
||||
private final TextView unreadCount;
|
||||
private final ImageView scrollButton;
|
||||
|
||||
public ConversationScrollToView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ConversationScrollToView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ConversationScrollToView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
inflate(context, R.layout.conversation_scroll_to, this);
|
||||
|
||||
unreadCount = findViewById(R.id.conversation_scroll_to_count);
|
||||
scrollButton = findViewById(R.id.conversation_scroll_to_button);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ConversationScrollToView);
|
||||
int srcId = array.getResourceId(R.styleable.ConversationScrollToView_cstv_scroll_button_src, 0);
|
||||
|
||||
scrollButton.setImageResource(srcId);
|
||||
|
||||
array.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(@Nullable OnClickListener l) {
|
||||
scrollButton.setOnClickListener(l);
|
||||
}
|
||||
|
||||
public void setUnreadCount(int unreadCount) {
|
||||
this.unreadCount.setText(formatUnreadCount(unreadCount));
|
||||
this.unreadCount.setVisibility(unreadCount > 0 ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
private @NonNull CharSequence formatUnreadCount(int unreadCount) {
|
||||
return unreadCount > 999 ? "999+" : String.valueOf(unreadCount);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@ package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
@@ -36,6 +34,7 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -94,7 +93,6 @@ public class InputPanel extends LinearLayout
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
@@ -160,7 +158,7 @@ public class InputPanel extends LinearLayout
|
||||
public void setQuote(@NonNull GlideRequests glideRequests,
|
||||
long id,
|
||||
@NonNull Recipient author,
|
||||
@NonNull String body,
|
||||
@NonNull CharSequence body,
|
||||
@NonNull SlideDeck attachments)
|
||||
{
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
|
||||
@@ -228,7 +226,7 @@ public class InputPanel extends LinearLayout
|
||||
|
||||
public Optional<QuoteModel> getQuote() {
|
||||
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
|
||||
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody(), false, quoteView.getAttachments()));
|
||||
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions()));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
@@ -239,6 +237,11 @@ public class InputPanel extends LinearLayout
|
||||
this.linkPreview.setLoading();
|
||||
}
|
||||
|
||||
public void setLinkPreviewNoPreview(@Nullable LinkPreviewRepository.Error customError) {
|
||||
this.linkPreview.setVisibility(View.VISIBLE);
|
||||
this.linkPreview.setNoPreview(customError);
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull Optional<LinkPreview> preview) {
|
||||
if (preview.isPresent()) {
|
||||
this.linkPreview.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -4,23 +4,34 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
/**
|
||||
* The view shown in the compose box or conversation that represents the state of the link preview.
|
||||
*/
|
||||
public class LinkPreviewView extends FrameLayout {
|
||||
|
||||
private static final int TYPE_CONVERSATION = 0;
|
||||
@@ -29,10 +40,12 @@ public class LinkPreviewView extends FrameLayout {
|
||||
private ViewGroup container;
|
||||
private OutlinedThumbnailView thumbnail;
|
||||
private TextView title;
|
||||
private TextView description;
|
||||
private TextView site;
|
||||
private View divider;
|
||||
private View closeButton;
|
||||
private View spinner;
|
||||
private TextView noPreview;
|
||||
|
||||
private int type;
|
||||
private int defaultRadius;
|
||||
@@ -56,10 +69,12 @@ public class LinkPreviewView extends FrameLayout {
|
||||
container = findViewById(R.id.linkpreview_container);
|
||||
thumbnail = findViewById(R.id.linkpreview_thumbnail);
|
||||
title = findViewById(R.id.linkpreview_title);
|
||||
description = findViewById(R.id.linkpreview_description);
|
||||
site = findViewById(R.id.linkpreview_site);
|
||||
divider = findViewById(R.id.linkpreview_divider);
|
||||
spinner = findViewById(R.id.linkpreview_progress_wheel);
|
||||
closeButton = findViewById(R.id.linkpreview_close);
|
||||
noPreview = findViewById(R.id.linkpreview_no_preview);
|
||||
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
@@ -77,6 +92,8 @@ public class LinkPreviewView extends FrameLayout {
|
||||
container.setPadding(0, 0, 0, 0);
|
||||
divider.setVisibility(VISIBLE);
|
||||
closeButton.setVisibility(VISIBLE);
|
||||
title.setMaxLines(2);
|
||||
description.setMaxLines(2);
|
||||
|
||||
closeButton.setOnClickListener(v -> {
|
||||
if (closeClickedListener != null) {
|
||||
@@ -100,21 +117,59 @@ public class LinkPreviewView extends FrameLayout {
|
||||
public void setLoading() {
|
||||
title.setVisibility(GONE);
|
||||
site.setVisibility(GONE);
|
||||
description.setVisibility(GONE);
|
||||
thumbnail.setVisibility(GONE);
|
||||
spinner.setVisibility(VISIBLE);
|
||||
noPreview.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
public void setNoPreview(@Nullable LinkPreviewRepository.Error customError) {
|
||||
title.setVisibility(GONE);
|
||||
site.setVisibility(GONE);
|
||||
thumbnail.setVisibility(GONE);
|
||||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(VISIBLE);
|
||||
noPreview.setText(getLinkPreviewErrorString(customError));
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||
title.setVisibility(VISIBLE);
|
||||
site.setVisibility(VISIBLE);
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(GONE);
|
||||
|
||||
title.setText(linkPreview.getTitle());
|
||||
if (!Util.isEmpty(linkPreview.getTitle())) {
|
||||
title.setText(linkPreview.getTitle());
|
||||
title.setVisibility(VISIBLE);
|
||||
} else {
|
||||
title.setVisibility(GONE);
|
||||
}
|
||||
|
||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
||||
if (url != null) {
|
||||
site.setText(url.topPrivateDomain());
|
||||
if (!Util.isEmpty(linkPreview.getDescription())) {
|
||||
description.setText(linkPreview.getDescription());
|
||||
description.setVisibility(VISIBLE);
|
||||
} else {
|
||||
description.setVisibility(GONE);
|
||||
}
|
||||
|
||||
String domain = null;
|
||||
|
||||
if (!Util.isEmpty(linkPreview.getUrl())) {
|
||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
||||
if (url != null) {
|
||||
domain = url.topPrivateDomain();
|
||||
}
|
||||
}
|
||||
|
||||
if (domain != null && linkPreview.getDate() > 0) {
|
||||
site.setText(getContext().getString(R.string.LinkPreviewView_domain_date, domain, formatDate(linkPreview.getDate())));
|
||||
site.setVisibility(VISIBLE);
|
||||
} else if (domain != null) {
|
||||
site.setText(domain);
|
||||
site.setVisibility(VISIBLE);
|
||||
} else if (linkPreview.getDate() > 0) {
|
||||
site.setText(formatDate(linkPreview.getDate()));
|
||||
site.setVisibility(VISIBLE);
|
||||
} else {
|
||||
site.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
||||
@@ -141,6 +196,16 @@ public class LinkPreviewView extends FrameLayout {
|
||||
thumbnail.setDownloadClickListener(listener);
|
||||
}
|
||||
|
||||
private @StringRes static int getLinkPreviewErrorString(@Nullable LinkPreviewRepository.Error customError) {
|
||||
return customError == LinkPreviewRepository.Error.GROUP_LINK_INACTIVE ? R.string.LinkPreviewView_this_group_link_is_not_active
|
||||
: R.string.LinkPreviewView_no_link_preview_available;
|
||||
}
|
||||
|
||||
private static String formatDate(long date) {
|
||||
DateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy", Locale.getDefault());
|
||||
return dateFormat.format(date);
|
||||
}
|
||||
|
||||
public interface CloseClickedListener {
|
||||
void onCloseClicked();
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ public class MaskView extends View {
|
||||
target.getDrawingRect(drawingRect);
|
||||
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
|
||||
|
||||
drawingRect.bottom = Math.min(drawingRect.bottom, getBottom() - getPaddingBottom());
|
||||
drawingRect.top += targetParentTranslationY;
|
||||
drawingRect.bottom += targetParentTranslationY;
|
||||
|
||||
@@ -88,6 +87,7 @@ public class MaskView extends View {
|
||||
|
||||
target.draw(maskCanvas);
|
||||
|
||||
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
||||
canvas.drawBitmap(mask, 0, drawingRect.top, maskPaint);
|
||||
|
||||
mask.recycle();
|
||||
|
||||
@@ -25,6 +25,14 @@ public class Outliner {
|
||||
outlinePaint.setColor(color);
|
||||
}
|
||||
|
||||
public void setStrokeWidth(float pixels) {
|
||||
outlinePaint.setStrokeWidth(pixels);
|
||||
}
|
||||
|
||||
public void setAlpha(int alpha) {
|
||||
outlinePaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
draw(canvas, 0, canvas.getWidth(), canvas.getHeight(), 0);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
@@ -55,7 +57,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
private long id;
|
||||
private LiveRecipient author;
|
||||
private String body;
|
||||
private CharSequence body;
|
||||
private TextView mediaDescriptionText;
|
||||
private TextView missingLinkText;
|
||||
private SlideDeck attachments;
|
||||
@@ -147,7 +149,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
public void setQuote(GlideRequests glideRequests,
|
||||
long id,
|
||||
@NonNull Recipient author,
|
||||
@Nullable String body,
|
||||
@Nullable CharSequence body,
|
||||
boolean originalMissing,
|
||||
@NonNull SlideDeck attachments)
|
||||
{
|
||||
@@ -196,7 +198,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
mainView.setBackgroundColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
|
||||
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
|
||||
bodyView.setVisibility(VISIBLE);
|
||||
bodyView.setText(body == null ? "" : body);
|
||||
@@ -280,11 +282,15 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
return author.get();
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
public CharSequence getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments.asAttachments();
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return MentionAnnotation.getMentionsFromAnnotations(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,11 @@ public class ThumbnailView extends FrameLayout {
|
||||
captionIcon.setScaleY(captionIconScale);
|
||||
}
|
||||
|
||||
public void setMinimumThumbnailWidth(int width) {
|
||||
bounds[MIN_WIDTH] = width;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
|
||||
int dimensFilledCount = getNonZeroCount(dimens);
|
||||
|
||||
@@ -37,6 +37,12 @@ public class ZoomingImageView extends FrameLayout {
|
||||
|
||||
private static final String TAG = ZoomingImageView.class.getSimpleName();
|
||||
|
||||
private static final int ZOOM_TRANSITION_DURATION = 300;
|
||||
|
||||
private static final float ZOOM_LEVEL_MIN = 1.0f;
|
||||
private static final float ZOOM_LEVEL_MID = 1.5f;
|
||||
private static final float ZOOM_LEVEL_MAX = 2.0f;
|
||||
|
||||
private final PhotoView photoView;
|
||||
private final SubsamplingScaleImageView subsamplingImageView;
|
||||
|
||||
@@ -58,6 +64,12 @@ public class ZoomingImageView extends FrameLayout {
|
||||
|
||||
this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
|
||||
|
||||
this.photoView.setZoomTransitionDuration(ZOOM_TRANSITION_DURATION);
|
||||
this.photoView.setScaleLevels(ZOOM_LEVEL_MIN, ZOOM_LEVEL_MID, ZOOM_LEVEL_MAX);
|
||||
|
||||
this.subsamplingImageView.setDoubleTapZoomDuration(ZOOM_TRANSITION_DURATION);
|
||||
this.subsamplingImageView.setDoubleTapZoomScale(ZOOM_LEVEL_MID);
|
||||
|
||||
this.photoView.setOnClickListener(v -> ZoomingImageView.this.callOnClick());
|
||||
this.subsamplingImageView.setOnClickListener(v -> ZoomingImageView.this.callOnClick());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
@@ -48,6 +49,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
recentModel.onCodePointSelected(emoji);
|
||||
SignalStore.emojiValues().setPreferredVariation(emoji);
|
||||
|
||||
if (emojiEventListener != null) {
|
||||
emojiEventListener.onEmojiSelected(emoji);
|
||||
|
||||
@@ -2,23 +2,33 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import android.text.Annotation;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
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.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
@@ -35,6 +45,9 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private int maxLength;
|
||||
private CharSequence overflowText;
|
||||
private CharSequence previousOverflowText;
|
||||
private boolean renderMentions;
|
||||
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
|
||||
public EmojiTextView(Context context) {
|
||||
this(context, null);
|
||||
@@ -48,14 +61,33 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
||||
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
||||
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||
renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true);
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
|
||||
originalFontSize = a.getDimensionPixelSize(0, 0);
|
||||
a.recycle();
|
||||
|
||||
if (renderMentions) {
|
||||
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.transparent_black_20));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (renderMentions && getText() instanceof Spanned && getLayout() != null) {
|
||||
int checkpoint = canvas.save();
|
||||
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
|
||||
try {
|
||||
mentionRendererDelegate.draw(canvas, (Spanned) getText(), getLayout());
|
||||
} finally {
|
||||
canvas.restoreToCount(checkpoint);
|
||||
}
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
@@ -115,7 +147,19 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private void ellipsizeAnyTextForMaxLength() {
|
||||
if (maxLength > 0 && getText().length() > maxLength + 1) {
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, maxLength)).append(ELLIPSIS).append(Optional.fromNullable(overflowText).or(""));
|
||||
|
||||
CharSequence shortenedText = getText().subSequence(0, maxLength);
|
||||
if (shortenedText instanceof Spanned) {
|
||||
Spanned spanned = (Spanned) shortenedText;
|
||||
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(spanned, maxLength - 1, maxLength);
|
||||
if (!mentionAnnotations.isEmpty()) {
|
||||
shortenedText = shortenedText.subSequence(0, spanned.getSpanStart(mentionAnnotations.get(0)));
|
||||
}
|
||||
}
|
||||
|
||||
newContent.append(shortenedText)
|
||||
.append(ELLIPSIS)
|
||||
.append(Util.emptyIfNull(overflowText));
|
||||
|
||||
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
|
||||
|
||||
@@ -198,4 +242,10 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
this.originalFontSize = TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics());
|
||||
super.setTextSize(unit, size);
|
||||
}
|
||||
|
||||
public void setMentionBackgroundTint(@ColorInt int mentionBackgroundTint) {
|
||||
if (renderMentions) {
|
||||
mentionRendererDelegate.setTint(mentionBackgroundTint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.PopupWindow;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -27,7 +28,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow {
|
||||
this.listener = listener;
|
||||
this.list = (ViewGroup) getContentView().findViewById(R.id.emoji_variation_container);
|
||||
|
||||
setBackgroundDrawable(null);
|
||||
setBackgroundDrawable(ThemeUtil.getThemedDrawable(context, R.attr.emoji_variation_selector_background));
|
||||
setOutsideTouchable(true);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@@ -13,6 +15,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -73,6 +76,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onCodePointSelected(String emoji) {
|
||||
recentlyUsed.remove(emoji);
|
||||
recentlyUsed.add(emoji);
|
||||
@@ -84,22 +88,16 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
}
|
||||
|
||||
final LinkedHashSet<String> latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(preferenceName, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(preferenceName, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
}
|
||||
|
||||
private String[] toReversePrimitiveArray(@NonNull LinkedHashSet<String> emojiSet) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.components.mention;
|
||||
|
||||
|
||||
import android.text.Annotation;
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This wraps an Android standard {@link Annotation} so it can leverage the built in
|
||||
* span parceling for copy/paste. The annotation span contains the mentioned recipient's
|
||||
* id (in numerical form).
|
||||
*
|
||||
* Note: Do not extend Annotation or the parceling behavior will be lost.
|
||||
*/
|
||||
public final class MentionAnnotation {
|
||||
|
||||
public static final String MENTION_ANNOTATION = "mention";
|
||||
|
||||
private MentionAnnotation() {
|
||||
}
|
||||
|
||||
public static Annotation mentionAnnotationForRecipientId(@NonNull RecipientId id) {
|
||||
return new Annotation(MENTION_ANNOTATION, idToMentionAnnotationValue(id));
|
||||
}
|
||||
|
||||
public static String idToMentionAnnotationValue(@NonNull RecipientId id) {
|
||||
return String.valueOf(id.toLong());
|
||||
}
|
||||
|
||||
public static boolean isMentionAnnotation(@NonNull Annotation annotation) {
|
||||
return MENTION_ANNOTATION.equals(annotation.getKey());
|
||||
}
|
||||
|
||||
public static void setMentionAnnotations(Spannable body, List<Mention> mentions) {
|
||||
for (Mention mention : mentions) {
|
||||
body.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(mention.getRecipientId()), mention.getStart(), mention.getStart() + mention.getLength(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull List<Mention> getMentionsFromAnnotations(@Nullable CharSequence text) {
|
||||
if (text instanceof Spanned) {
|
||||
Spanned spanned = (Spanned) text;
|
||||
return Stream.of(getMentionAnnotations(spanned))
|
||||
.map(annotation -> {
|
||||
int spanStart = spanned.getSpanStart(annotation);
|
||||
int spanLength = spanned.getSpanEnd(annotation) - spanStart;
|
||||
return new Mention(RecipientId.from(annotation.getValue()), spanStart, spanLength);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static @NonNull List<Annotation> getMentionAnnotations(@NonNull Spanned spanned) {
|
||||
return getMentionAnnotations(spanned, 0, spanned.length());
|
||||
}
|
||||
|
||||
public static @NonNull List<Annotation> getMentionAnnotations(@NonNull Spanned spanned, int start, int end) {
|
||||
return Stream.of(spanned.getSpans(start, end, Annotation.class))
|
||||
.filter(MentionAnnotation::isMentionAnnotation)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.components.mention;
|
||||
|
||||
import android.text.Annotation;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
|
||||
|
||||
/**
|
||||
* Detects if some part of the mention is being deleted, and if so, deletes the entire mention and
|
||||
* span from the text view.
|
||||
*/
|
||||
public class MentionDeleter implements TextWatcher {
|
||||
|
||||
@Nullable private Annotation toDelete;
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence sequence, int start, int count, int after) {
|
||||
if (count > 0 && sequence instanceof Spanned) {
|
||||
Spanned text = (Spanned) sequence;
|
||||
|
||||
for (Annotation annotation : MentionAnnotation.getMentionAnnotations(text, start, start + count)) {
|
||||
if (text.getSpanStart(annotation) < start && text.getSpanEnd(annotation) > start) {
|
||||
toDelete = annotation;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
if (toDelete == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int toDeleteStart = editable.getSpanStart(toDelete);
|
||||
int toDeleteEnd = editable.getSpanEnd(toDelete);
|
||||
editable.removeSpan(toDelete);
|
||||
toDelete = null;
|
||||
|
||||
editable.replace(toDeleteStart, toDeleteEnd, String.valueOf(MENTION_STARTER));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence sequence, int start, int before, int count) { }
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.thoughtcrime.securesms.components.mention;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Layout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.LayoutUtil;
|
||||
|
||||
/**
|
||||
* Handles actually drawing the mention backgrounds for a TextView.
|
||||
* <p>
|
||||
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||
*/
|
||||
public abstract class MentionRenderer {
|
||||
|
||||
protected final int horizontalPadding;
|
||||
protected final int verticalPadding;
|
||||
|
||||
public MentionRenderer(int horizontalPadding, int verticalPadding) {
|
||||
this.horizontalPadding = horizontalPadding;
|
||||
this.verticalPadding = verticalPadding;
|
||||
}
|
||||
|
||||
public abstract void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset);
|
||||
|
||||
protected int getLineTop(@NonNull Layout layout, int line) {
|
||||
return LayoutUtil.getLineTopWithoutPadding(layout, line) - verticalPadding;
|
||||
}
|
||||
|
||||
protected int getLineBottom(@NonNull Layout layout, int line) {
|
||||
return LayoutUtil.getLineBottomWithoutPadding(layout, line) + verticalPadding;
|
||||
}
|
||||
|
||||
public static final class SingleLineMentionRenderer extends MentionRenderer {
|
||||
|
||||
private final Drawable drawable;
|
||||
|
||||
public SingleLineMentionRenderer(int horizontalPadding, int verticalPadding, @NonNull Drawable drawable) {
|
||||
super(horizontalPadding, verticalPadding);
|
||||
this.drawable = drawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset) {
|
||||
int lineTop = getLineTop(layout, startLine);
|
||||
int lineBottom = getLineBottom(layout, startLine);
|
||||
int left = Math.min(startOffset, endOffset);
|
||||
int right = Math.max(startOffset, endOffset);
|
||||
|
||||
drawable.setBounds(left, lineTop, right, lineBottom);
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MultiLineMentionRenderer extends MentionRenderer {
|
||||
|
||||
private final Drawable drawableLeft;
|
||||
private final Drawable drawableMid;
|
||||
private final Drawable drawableRight;
|
||||
|
||||
public MultiLineMentionRenderer(int horizontalPadding, int verticalPadding,
|
||||
@NonNull Drawable drawableLeft,
|
||||
@NonNull Drawable drawableMid,
|
||||
@NonNull Drawable drawableRight)
|
||||
{
|
||||
super(horizontalPadding, verticalPadding);
|
||||
this.drawableLeft = drawableLeft;
|
||||
this.drawableMid = drawableMid;
|
||||
this.drawableRight = drawableRight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset) {
|
||||
int paragraphDirection = layout.getParagraphDirection(startLine);
|
||||
|
||||
float lineEndOffset;
|
||||
if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) {
|
||||
lineEndOffset = layout.getLineLeft(startLine) - horizontalPadding;
|
||||
} else {
|
||||
lineEndOffset = layout.getLineRight(startLine) + horizontalPadding;
|
||||
}
|
||||
|
||||
int lineBottom = getLineBottom(layout, startLine);
|
||||
int lineTop = getLineTop(layout, startLine);
|
||||
drawStart(canvas, startOffset, lineTop, (int) lineEndOffset, lineBottom);
|
||||
|
||||
for (int line = startLine + 1; line < endLine; line++) {
|
||||
int left = (int) layout.getLineLeft(line) - horizontalPadding;
|
||||
int right = (int) layout.getLineRight(line) + horizontalPadding;
|
||||
|
||||
lineTop = getLineTop(layout, line);
|
||||
lineBottom = getLineBottom(layout, line);
|
||||
|
||||
drawableMid.setBounds(left, lineTop, right, lineBottom);
|
||||
drawableMid.draw(canvas);
|
||||
}
|
||||
|
||||
float lineStartOffset;
|
||||
if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) {
|
||||
lineStartOffset = layout.getLineRight(startLine) + horizontalPadding;
|
||||
} else {
|
||||
lineStartOffset = layout.getLineLeft(startLine) - horizontalPadding;
|
||||
}
|
||||
|
||||
lineBottom = getLineBottom(layout, endLine);
|
||||
lineTop = getLineTop(layout, endLine);
|
||||
|
||||
drawEnd(canvas, (int) lineStartOffset, lineTop, endOffset, lineBottom);
|
||||
}
|
||||
|
||||
private void drawStart(@NonNull Canvas canvas, int start, int top, int end, int bottom) {
|
||||
if (start > end) {
|
||||
drawableRight.setBounds(end, top, start, bottom);
|
||||
drawableRight.draw(canvas);
|
||||
} else {
|
||||
drawableLeft.setBounds(start, top, end, bottom);
|
||||
drawableLeft.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawEnd(@NonNull Canvas canvas, int start, int top, int end, int bottom) {
|
||||
if (start > end) {
|
||||
drawableLeft.setBounds(end, top, start, bottom);
|
||||
drawableLeft.draw(canvas);
|
||||
} else {
|
||||
drawableRight.setBounds(start, top, end, bottom);
|
||||
drawableRight.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.components.mention;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Annotation;
|
||||
import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* Encapsulates the logic for determining the type of mention rendering needed (single vs multi-line) and then
|
||||
* passing that information to the appropriate {@link MentionRenderer}.
|
||||
* <p></p>
|
||||
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||
*/
|
||||
public class MentionRendererDelegate {
|
||||
|
||||
private final MentionRenderer single;
|
||||
private final MentionRenderer multi;
|
||||
private final int horizontalPadding;
|
||||
private final Drawable drawable;
|
||||
private final Drawable drawableLeft;
|
||||
private final Drawable drawableMid;
|
||||
private final Drawable drawableEnd;
|
||||
|
||||
public MentionRendererDelegate(@NonNull Context context, @ColorInt int tint) {
|
||||
this.horizontalPadding = ViewUtil.dpToPx(2);
|
||||
|
||||
drawable = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg), tint);
|
||||
drawableLeft = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_left), tint);
|
||||
drawableMid = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_mid), tint);
|
||||
drawableEnd = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_right), tint);
|
||||
|
||||
single = new MentionRenderer.SingleLineMentionRenderer(horizontalPadding,
|
||||
0,
|
||||
drawable);
|
||||
|
||||
multi = new MentionRenderer.MultiLineMentionRenderer(horizontalPadding,
|
||||
0,
|
||||
drawableLeft,
|
||||
drawableMid,
|
||||
drawableEnd);
|
||||
}
|
||||
|
||||
public void draw(@NonNull Canvas canvas, @NonNull Spanned text, @NonNull Layout layout) {
|
||||
Annotation[] annotations = text.getSpans(0, text.length(), Annotation.class);
|
||||
for (Annotation annotation : annotations) {
|
||||
if (MentionAnnotation.isMentionAnnotation(annotation)) {
|
||||
int spanStart = text.getSpanStart(annotation);
|
||||
int spanEnd = text.getSpanEnd(annotation);
|
||||
int startLine = layout.getLineForOffset(spanStart);
|
||||
int endLine = layout.getLineForOffset(spanEnd);
|
||||
|
||||
int startOffset = (int) (layout.getPrimaryHorizontal(spanStart) + -1 * layout.getParagraphDirection(startLine) * horizontalPadding);
|
||||
int endOffset = (int) (layout.getPrimaryHorizontal(spanEnd) + layout.getParagraphDirection(endLine) * horizontalPadding);
|
||||
|
||||
MentionRenderer renderer = (startLine == endLine) ? single : multi;
|
||||
renderer.draw(canvas, layout, startLine, endLine, startOffset, endOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setTint(@ColorInt int tint) {
|
||||
DrawableCompat.setTint(drawable, tint);
|
||||
DrawableCompat.setTint(drawableLeft, tint);
|
||||
DrawableCompat.setTint(drawableMid, tint);
|
||||
DrawableCompat.setTint(drawableEnd, tint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.components.mention;
|
||||
|
||||
import android.text.Annotation;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides a mechanism to validate mention annotations set on an edit text. This enables
|
||||
* removing invalid mentions if the user mentioned isn't in the group.
|
||||
*/
|
||||
public class MentionValidatorWatcher implements TextWatcher {
|
||||
|
||||
@Nullable private List<Annotation> invalidMentionAnnotations;
|
||||
@Nullable private MentionValidator mentionValidator;
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence sequence, int start, int before, int count) {
|
||||
if (count > 1 && mentionValidator != null && sequence instanceof Spanned) {
|
||||
Spanned span = (Spanned) sequence;
|
||||
|
||||
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(span, start, start + count);
|
||||
|
||||
if (mentionAnnotations.size() > 0) {
|
||||
invalidMentionAnnotations = mentionValidator.getInvalidMentionAnnotations(mentionAnnotations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
if (invalidMentionAnnotations == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Annotation> invalidMentions = invalidMentionAnnotations;
|
||||
invalidMentionAnnotations = null;
|
||||
|
||||
for (Annotation annotation : invalidMentions) {
|
||||
editable.removeSpan(annotation);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMentionValidator(@Nullable MentionValidator mentionValidator) {
|
||||
this.mentionValidator = mentionValidator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence sequence, int start, int count, int after) { }
|
||||
|
||||
public interface MentionValidator {
|
||||
List<Annotation> getInvalidMentionAnnotations(List<Annotation> mentionAnnotations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.components.qr;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SquareImageView;
|
||||
import org.thoughtcrime.securesms.qr.QrCode;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Generates a bitmap asynchronously for the supplied {@link BitMatrix} data and displays it.
|
||||
*/
|
||||
public class QrView extends SquareImageView {
|
||||
|
||||
private static final @ColorInt int DEFAULT_FOREGROUND_COLOR = Color.BLACK;
|
||||
private static final @ColorInt int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
|
||||
|
||||
private @Nullable Bitmap qrBitmap;
|
||||
private @ColorInt int foregroundColor;
|
||||
private @ColorInt int backgroundColor;
|
||||
|
||||
public QrView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
public QrView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
public QrView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QrView, 0, 0);
|
||||
foregroundColor = typedArray.getColor(R.styleable.QrView_qr_foreground_color, DEFAULT_FOREGROUND_COLOR);
|
||||
backgroundColor = typedArray.getColor(R.styleable.QrView_qr_background_color, DEFAULT_BACKGROUND_COLOR);
|
||||
typedArray.recycle();
|
||||
} else {
|
||||
foregroundColor = DEFAULT_FOREGROUND_COLOR;
|
||||
backgroundColor = DEFAULT_BACKGROUND_COLOR;
|
||||
}
|
||||
|
||||
if (isInEditMode()) {
|
||||
setQrText("https://signal.org");
|
||||
}
|
||||
}
|
||||
|
||||
public void setQrText(@Nullable String text) {
|
||||
setQrBitmap(QrCode.create(text, foregroundColor, backgroundColor));
|
||||
}
|
||||
|
||||
private void setQrBitmap(@Nullable Bitmap qrBitmap) {
|
||||
if (this.qrBitmap == qrBitmap) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.qrBitmap != null) {
|
||||
this.qrBitmap.recycle();
|
||||
}
|
||||
|
||||
this.qrBitmap = qrBitmap;
|
||||
|
||||
setImageBitmap(this.qrBitmap);
|
||||
}
|
||||
|
||||
public @Nullable Bitmap getQrBitmap() {
|
||||
return qrBitmap;
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,11 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CallMeCountDownView extends androidx.appcompat.widget.AppCompatButton {
|
||||
|
||||
private int countDown;
|
||||
private long countDownToTime;
|
||||
@Nullable
|
||||
private Listener listener;
|
||||
|
||||
@@ -26,9 +28,14 @@ public class CallMeCountDownView extends androidx.appcompat.widget.AppCompatButt
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void startCountDown(int countDown) {
|
||||
this.countDown = countDown;
|
||||
updateCountDown();
|
||||
/**
|
||||
* Starts a count down to the specified {@param time}.
|
||||
*/
|
||||
public void startCountDownTo(long time) {
|
||||
if (time > 0) {
|
||||
this.countDownToTime = time;
|
||||
updateCountDown();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCallEnabled() {
|
||||
@@ -38,23 +45,24 @@ public class CallMeCountDownView extends androidx.appcompat.widget.AppCompatButt
|
||||
}
|
||||
|
||||
private void updateCountDown() {
|
||||
if (countDown > 0) {
|
||||
final long remainingMillis = countDownToTime - System.currentTimeMillis();
|
||||
|
||||
if (remainingMillis > 0) {
|
||||
setEnabled(false);
|
||||
setAlpha(0.5f);
|
||||
|
||||
countDown--;
|
||||
|
||||
int minutesRemaining = countDown / 60;
|
||||
int secondsRemaining = countDown % 60;
|
||||
int totalRemainingSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(remainingMillis);
|
||||
int minutesRemaining = totalRemainingSeconds / 60;
|
||||
int secondsRemaining = totalRemainingSeconds % 60;
|
||||
|
||||
setText(getResources().getString(R.string.RegistrationActivity_call_me_instead_available_in, minutesRemaining, secondsRemaining));
|
||||
|
||||
if (listener != null) {
|
||||
listener.onRemaining(this, countDown);
|
||||
listener.onRemaining(this, totalRemainingSeconds);
|
||||
}
|
||||
|
||||
postDelayed(this::updateCountDown, 1000);
|
||||
} else if (countDown == 0) {
|
||||
postDelayed(this::updateCountDown, 250);
|
||||
} else {
|
||||
setCallEnabled();
|
||||
}
|
||||
}
|
||||
@@ -64,6 +72,6 @@ public class CallMeCountDownView extends androidx.appcompat.widget.AppCompatButt
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onRemaining(@NonNull CallMeCountDownView view, int remaining);
|
||||
void onRemaining(@NonNull CallMeCountDownView view, int secondsRemaining);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Showed when a build has fully expired (either via the compile-time constant, or remote
|
||||
* deprecation).
|
||||
*/
|
||||
public class ExpiredBuildReminder extends Reminder {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ExpiredBuildReminder.class.getSimpleName();
|
||||
|
||||
public ExpiredBuildReminder(final Context context) {
|
||||
super(context.getString(R.string.reminder_header_expired_build),
|
||||
context.getString(R.string.reminder_header_expired_build_details));
|
||||
setOkListener(v -> {
|
||||
try {
|
||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName())));
|
||||
} catch (android.content.ActivityNotFoundException anfe) {
|
||||
try {
|
||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + context.getPackageName())));
|
||||
} catch (android.content.ActivityNotFoundException anfe2) {
|
||||
Log.w(TAG, anfe2);
|
||||
Toast.makeText(context, R.string.OutdatedBuildReminder_no_web_browser_installed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
super(null, context.getString(R.string.ExpiredBuildReminder_this_version_of_signal_has_expired));
|
||||
|
||||
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
|
||||
addAction(new Action(context.getString(R.string.ExpiredBuildReminder_update_now), R.id.reminder_action_update_now));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,8 +28,17 @@ public class ExpiredBuildReminder extends Reminder {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return Util.getDaysTillBuildExpiry() <= 0;
|
||||
@Override
|
||||
public List<Action> getActions() {
|
||||
return super.getActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Importance getImportance() {
|
||||
return Importance.TERMINAL;
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return SignalStore.misc().isClientDeprecated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,29 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Reminder that is shown when a build is getting close to expiry (either because of the
|
||||
* compile-time constant, or remote deprecation).
|
||||
*/
|
||||
public class OutdatedBuildReminder extends Reminder {
|
||||
|
||||
private static final String TAG = OutdatedBuildReminder.class.getSimpleName();
|
||||
|
||||
public OutdatedBuildReminder(final Context context) {
|
||||
super(context.getString(R.string.reminder_header_outdated_build),
|
||||
getPluralsText(context));
|
||||
setOkListener(v -> {
|
||||
try {
|
||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName())));
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
try {
|
||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + context.getPackageName())));
|
||||
} catch (ActivityNotFoundException anfe2) {
|
||||
Log.w(TAG, anfe2);
|
||||
Toast.makeText(context, R.string.OutdatedBuildReminder_no_web_browser_installed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
super(null, getPluralsText(context));
|
||||
|
||||
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
|
||||
addAction(new Action(context.getString(R.string.OutdatedBuildReminder_update_now), R.id.reminder_action_update_now));
|
||||
}
|
||||
|
||||
private static CharSequence getPluralsText(final Context context) {
|
||||
int days = Util.getDaysTillBuildExpiry() - 1;
|
||||
if (days == 0) {
|
||||
return context.getString(R.string.reminder_header_outdated_build_details_today);
|
||||
}
|
||||
return context.getResources().getQuantityString(R.plurals.reminder_header_outdated_build_details, days, days);
|
||||
int days = getDaysUntilExpiry() - 1;
|
||||
return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,7 +32,10 @@ public class OutdatedBuildReminder extends Reminder {
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return Util.getDaysTillBuildExpiry() <= 10;
|
||||
return getDaysUntilExpiry() <= 10;
|
||||
}
|
||||
|
||||
private static int getDaysUntilExpiry() {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(Util.getTimeUntilBuildExpiry());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public abstract class Reminder {
|
||||
return Importance.NORMAL;
|
||||
}
|
||||
|
||||
public void addAction(@NonNull Action action) {
|
||||
protected void addAction(@NonNull Action action) {
|
||||
actions.add(action);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ public abstract class Reminder {
|
||||
}
|
||||
|
||||
public enum Importance {
|
||||
NORMAL, ERROR
|
||||
NORMAL, ERROR, TERMINAL
|
||||
}
|
||||
|
||||
public final class Action {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -19,7 +17,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -48,7 +45,6 @@ public final class ReminderView extends FrameLayout {
|
||||
initialize();
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.HONEYCOMB)
|
||||
public ReminderView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
@@ -56,14 +52,14 @@ public final class ReminderView extends FrameLayout {
|
||||
|
||||
private void initialize() {
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.reminder_header, this, true);
|
||||
progressBar = ViewUtil.findById(this, R.id.reminder_progress);
|
||||
progressText = ViewUtil.findById(this, R.id.reminder_progress_text);
|
||||
container = ViewUtil.findById(this, R.id.container);
|
||||
closeButton = ViewUtil.findById(this, R.id.cancel);
|
||||
title = ViewUtil.findById(this, R.id.reminder_title);
|
||||
text = ViewUtil.findById(this, R.id.reminder_text);
|
||||
space = ViewUtil.findById(this, R.id.reminder_space);
|
||||
actionsRecycler = ViewUtil.findById(this, R.id.reminder_actions);
|
||||
progressBar = findViewById(R.id.reminder_progress);
|
||||
progressText = findViewById(R.id.reminder_progress_text);
|
||||
container = findViewById(R.id.container);
|
||||
closeButton = findViewById(R.id.cancel);
|
||||
title = findViewById(R.id.reminder_title);
|
||||
text = findViewById(R.id.reminder_text);
|
||||
space = findViewById(R.id.reminder_space);
|
||||
actionsRecycler = findViewById(R.id.reminder_actions);
|
||||
}
|
||||
|
||||
public void showReminder(final Reminder reminder) {
|
||||
@@ -76,9 +72,26 @@ public final class ReminderView extends FrameLayout {
|
||||
title.setVisibility(GONE);
|
||||
space.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (!reminder.isDismissable()) {
|
||||
space.setVisibility(GONE);
|
||||
}
|
||||
|
||||
text.setText(reminder.getText());
|
||||
container.setBackgroundResource(reminder.getImportance() == Reminder.Importance.ERROR ? R.drawable.reminder_background_error
|
||||
: R.drawable.reminder_background_normal);
|
||||
|
||||
switch (reminder.getImportance()) {
|
||||
case NORMAL:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_normal);
|
||||
break;
|
||||
case ERROR:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_error);
|
||||
break;
|
||||
case TERMINAL:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_terminal);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
setOnClickListener(reminder.getOkListener());
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
|
||||
/**
|
||||
* Reusable adapter for generic settings list.
|
||||
*/
|
||||
public class BaseSettingsAdapter extends MappingAdapter {
|
||||
public void configureSingleSelect(@NonNull SingleSelectSetting.SingleSelectSelectionChangedListener selectionChangedListener) {
|
||||
registerFactory(SingleSelectSetting.Item.class,
|
||||
new LayoutFactory<>(v -> new SingleSelectSetting.ViewHolder(v, selectionChangedListener), R.layout.single_select_item));
|
||||
}
|
||||
|
||||
public void configureCustomizableSingleSelect(@NonNull CustomizableSingleSelectSetting.CustomizableSingleSelectionListener selectionListener) {
|
||||
registerFactory(CustomizableSingleSelectSetting.Item.class,
|
||||
new LayoutFactory<>(v -> new CustomizableSingleSelectSetting.ViewHolder(v, selectionListener), R.layout.customizable_single_select_item));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModelList;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A simple settings screen that takes its configuration via {@link Configuration}.
|
||||
*/
|
||||
public class BaseSettingsFragment extends Fragment {
|
||||
|
||||
private static final String CONFIGURATION_ARGUMENT = "current_selection";
|
||||
|
||||
private RecyclerView recycler;
|
||||
|
||||
public static @NonNull BaseSettingsFragment create(@NonNull Configuration configuration) {
|
||||
BaseSettingsFragment fragment = new BaseSettingsFragment();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(CONFIGURATION_ARGUMENT, configuration);
|
||||
fragment.setArguments(arguments);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.base_settings_fragment, container, false);
|
||||
|
||||
recycler = view.findViewById(R.id.base_settings_list);
|
||||
recycler.setItemAnimator(null);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
BaseSettingsAdapter adapter = new BaseSettingsAdapter();
|
||||
|
||||
recycler.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
recycler.setAdapter(adapter);
|
||||
|
||||
Configuration configuration = (Configuration) Objects.requireNonNull(requireArguments().getSerializable(CONFIGURATION_ARGUMENT));
|
||||
configuration.configure(requireActivity(), adapter);
|
||||
configuration.setArguments(getArguments());
|
||||
configuration.configureAdapter(adapter);
|
||||
|
||||
adapter.submitList(configuration.getSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration for a settings screen. Utilizes serializable to hide
|
||||
* reflection of instantiating from a fragment argument.
|
||||
*/
|
||||
public static abstract class Configuration implements Serializable {
|
||||
protected transient FragmentActivity activity;
|
||||
protected transient BaseSettingsAdapter adapter;
|
||||
|
||||
public void configure(@NonNull FragmentActivity activity, @NonNull BaseSettingsAdapter adapter) {
|
||||
this.activity = activity;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve any runtime information from the fragment's arguments.
|
||||
*/
|
||||
public void setArguments(@Nullable Bundle arguments) {}
|
||||
|
||||
protected void updateSettingsList() {
|
||||
adapter.submitList(getSettings());
|
||||
}
|
||||
|
||||
public abstract void configureAdapter(@NonNull BaseSettingsAdapter adapter);
|
||||
|
||||
public abstract @NonNull MappingModelList getSettings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Adds ability to customize a value for a single select (radio) setting.
|
||||
*/
|
||||
public class CustomizableSingleSelectSetting {
|
||||
|
||||
public interface CustomizableSingleSelectionListener extends SingleSelectSetting.SingleSelectSelectionChangedListener {
|
||||
void onCustomizeClicked(@NonNull Item item);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends MappingViewHolder<Item> {
|
||||
private final TextView summaryText;
|
||||
private final View customize;
|
||||
private final RadioButton radio;
|
||||
private final SingleSelectSetting.ViewHolder delegate;
|
||||
private final Group customizeGroup;
|
||||
private final CustomizableSingleSelectionListener selectionListener;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull CustomizableSingleSelectionListener selectionListener) {
|
||||
super(itemView);
|
||||
this.selectionListener = selectionListener;
|
||||
|
||||
radio = findViewById(R.id.customizable_single_select_radio);
|
||||
summaryText = findViewById(R.id.customizable_single_select_summary);
|
||||
customize = findViewById(R.id.customizable_single_select_customize);
|
||||
customizeGroup = findViewById(R.id.customizable_single_select_customize_group);
|
||||
|
||||
delegate = new SingleSelectSetting.ViewHolder(itemView, selectionListener) {
|
||||
@Override
|
||||
protected void setChecked(boolean checked) {
|
||||
radio.setChecked(checked);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Item model) {
|
||||
delegate.bind(model.singleSelectItem);
|
||||
customizeGroup.setVisibility(radio.isChecked() ? View.VISIBLE : View.GONE);
|
||||
customize.setOnClickListener(v -> selectionListener.onCustomizeClicked(model));
|
||||
if (model.getCustomValue() != null) {
|
||||
summaryText.setText(model.getSummaryText());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Item implements MappingModel<Item> {
|
||||
private SingleSelectSetting.Item singleSelectItem;
|
||||
private Object customValue;
|
||||
private String summaryText;
|
||||
|
||||
public <T> Item(@NonNull T item, @Nullable String text, boolean isSelected, @Nullable Object customValue, @Nullable String summaryText) {
|
||||
this.customValue = customValue;
|
||||
this.summaryText = summaryText;
|
||||
|
||||
singleSelectItem = new SingleSelectSetting.Item(item, text, isSelected);
|
||||
}
|
||||
|
||||
public @Nullable Object getCustomValue() {
|
||||
return customValue;
|
||||
}
|
||||
|
||||
public @Nullable String getSummaryText() {
|
||||
return summaryText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Item newItem) {
|
||||
return singleSelectItem.areItemsTheSame(newItem.singleSelectItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Item newItem) {
|
||||
return singleSelectItem.areContentsTheSame(newItem.singleSelectItem) && Objects.equals(customValue, newItem.customValue) && Objects.equals(summaryText, newItem.summaryText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.CheckedTextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Single select (radio) setting option
|
||||
*/
|
||||
public class SingleSelectSetting {
|
||||
|
||||
public interface SingleSelectSelectionChangedListener {
|
||||
void onSelectionChanged(@NonNull Object selection);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends MappingViewHolder<Item> {
|
||||
|
||||
protected final CheckedTextView text;
|
||||
protected final SingleSelectSelectionChangedListener selectionChangedListener;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull SingleSelectSelectionChangedListener selectionChangedListener) {
|
||||
super(itemView);
|
||||
this.selectionChangedListener = selectionChangedListener;
|
||||
this.text = findViewById(R.id.single_select_item_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Item model) {
|
||||
text.setText(model.text);
|
||||
setChecked(model.isSelected);
|
||||
itemView.setOnClickListener(v -> selectionChangedListener.onSelectionChanged(model.item));
|
||||
}
|
||||
|
||||
protected void setChecked(boolean checked) {
|
||||
text.setChecked(checked);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Item implements MappingModel<Item> {
|
||||
private final String text;
|
||||
private final Object item;
|
||||
private final boolean isSelected;
|
||||
|
||||
public <T> Item(@NonNull T item, @Nullable String text, boolean isSelected) {
|
||||
this.item = item;
|
||||
this.text = text != null ? text : item.toString();
|
||||
this.isSelected = isSelected;
|
||||
}
|
||||
|
||||
public @NonNull String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public @NonNull Object getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Item newItem) {
|
||||
return item.equals(newItem.item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Item newItem) {
|
||||
return Objects.equals(text, newItem.text) && isSelected == newItem.isSelected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,12 +98,12 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
}
|
||||
|
||||
public void clearVerticalBoundaries() {
|
||||
setVerticalBoundaries(0, parent.getMeasuredHeight());
|
||||
setVerticalBoundaries(parent.getTop(), parent.getMeasuredHeight() + parent.getTop());
|
||||
}
|
||||
|
||||
public void setVerticalBoundaries(int topBoundary, int bottomBoundary) {
|
||||
extraPaddingTop = topBoundary;
|
||||
extraPaddingBottom = parent.getMeasuredHeight() - bottomBoundary;
|
||||
extraPaddingTop = topBoundary - parent.getTop();
|
||||
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
|
||||
|
||||
if (isAnimating) {
|
||||
fling();
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.constraintlayout.widget.Guideline;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.transition.AutoTransition;
|
||||
import androidx.transition.Transition;
|
||||
@@ -117,12 +118,13 @@ public class WebRtcCallView extends FrameLayout {
|
||||
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
|
||||
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient);
|
||||
|
||||
View topGradient = findViewById(R.id.call_screen_header_gradient);
|
||||
View downCaret = findViewById(R.id.call_screen_down_arrow);
|
||||
View decline = findViewById(R.id.call_screen_decline_call);
|
||||
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
|
||||
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
|
||||
View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient);
|
||||
View topGradient = findViewById(R.id.call_screen_header_gradient);
|
||||
View downCaret = findViewById(R.id.call_screen_down_arrow);
|
||||
View decline = findViewById(R.id.call_screen_decline_call);
|
||||
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
|
||||
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
|
||||
View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient);
|
||||
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
|
||||
|
||||
topViews.add(status);
|
||||
topViews.add(topGradient);
|
||||
@@ -166,11 +168,8 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame);
|
||||
|
||||
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
MarginLayoutParams params = (MarginLayoutParams) parent.getLayoutParams();
|
||||
|
||||
params.topMargin = statusBarHeight;
|
||||
parent.setLayoutParams(params);
|
||||
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
statusBarGuideline.setGuidelineBegin(statusBarHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Activity which displays a dialog to confirm whether to turn off "Contact Joined Signal" notifications.
|
||||
*/
|
||||
public class TurnOffContactJoinedNotificationsActivity extends AppCompatActivity {
|
||||
|
||||
private final static String EXTRA_THREAD_ID = "thread_id";
|
||||
|
||||
public static Intent newIntent(@NonNull Context context, long threadId) {
|
||||
Intent intent = new Intent(context, TurnOffContactJoinedNotificationsActivity.class);
|
||||
|
||||
intent.putExtra(EXTRA_THREAD_ID, threadId);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.TurnOffContactJoinedNotificationsActivity__turn_off_contact_joined_signal)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
handlePositiveAction(dialog);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
}))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void handlePositiveAction(@NonNull DialogInterface dialog) {
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this);
|
||||
|
||||
List<MessageDatabase.MarkedMessageInfo> marked = threadDatabase.setRead(getIntent().getLongExtra(EXTRA_THREAD_ID, -1), false);
|
||||
MarkReadReceiver.process(this, marked);
|
||||
|
||||
TextSecurePreferences.setNewContactsNotificationEnabled(this, false);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(this);
|
||||
|
||||
return null;
|
||||
}, unused -> {
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
@@ -37,14 +38,16 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientDetails;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@@ -93,62 +96,44 @@ public class DirectoryHelper {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Set<String> databaseNumbers = sanitizeNumbers(recipientDatabase.getAllPhoneNumbers());
|
||||
Set<String> systemNumbers = sanitizeNumbers(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
|
||||
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
|
||||
|
||||
DirectoryResult result;
|
||||
|
||||
if (FeatureFlags.cds()) {
|
||||
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
|
||||
} else {
|
||||
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
|
||||
}
|
||||
|
||||
if (result.getNumberRewrites().size() > 0) {
|
||||
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
|
||||
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
|
||||
}
|
||||
|
||||
Map<RecipientId, String> uuidMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
|
||||
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
|
||||
Set<RecipientId> activeIds = uuidMap.keySet();
|
||||
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
|
||||
.filterNot(activeNumbers::contains)
|
||||
.filterNot(n -> result.getNumberRewrites().containsKey(n))
|
||||
.map(recipientDatabase::getOrInsertFromE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
|
||||
|
||||
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
|
||||
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
|
||||
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
|
||||
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
|
||||
|
||||
newlyActiveIds.removeAll(existingSignalIds);
|
||||
newlyActiveIds.retainAll(existingSystemIds);
|
||||
|
||||
notifyNewUsers(context, newlyActiveIds);
|
||||
} else {
|
||||
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
|
||||
}
|
||||
refreshNumbers(context, databaseNumbers, systemNumbers, notifyOfNewUsers);
|
||||
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void refreshDirectoryFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean notifyOfNewUsers) throws IOException {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
if (recipient.hasUuid() && !recipient.hasE164()) {
|
||||
if (isUuidRegistered(context, recipient)) {
|
||||
recipientDatabase.markRegistered(recipient.getId(), recipient.requireUuid());
|
||||
} else {
|
||||
recipientDatabase.markUnregistered(recipient.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> numbers = Stream.of(recipients)
|
||||
.filter(Recipient::hasE164)
|
||||
.map(Recipient::requireE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
refreshNumbers(context, numbers, numbers, notifyOfNewUsers);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static RegisteredState refreshDirectoryFor(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException {
|
||||
Stopwatch stopwatch = new Stopwatch("single");
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
|
||||
RegisteredState newRegisteredState = null;
|
||||
|
||||
if (recipient.hasUuid() && !recipient.hasE164()) {
|
||||
boolean isRegistered = isUuidRegistered(context, recipient);
|
||||
stopwatch.split("uuid-network");
|
||||
if (isRegistered) {
|
||||
boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
|
||||
if (idChanged) {
|
||||
@@ -158,6 +143,9 @@ public class DirectoryHelper {
|
||||
recipientDatabase.markUnregistered(recipient.getId());
|
||||
}
|
||||
|
||||
stopwatch.split("uuid-disk");
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
|
||||
}
|
||||
|
||||
@@ -174,6 +162,8 @@ public class DirectoryHelper {
|
||||
result = ContactDiscoveryV1.getDirectoryResult(recipient.getE164().get());
|
||||
}
|
||||
|
||||
stopwatch.split("e164-network");
|
||||
|
||||
if (result.getNumberRewrites().size() > 0) {
|
||||
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
|
||||
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
|
||||
@@ -210,9 +200,79 @@ public class DirectoryHelper {
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
stopwatch.split("e164-disk");
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
return newRegisteredState;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static void refreshNumbers(@NonNull Context context, @NonNull Set<String> databaseNumbers, @NonNull Set<String> systemNumbers, boolean notifyOfNewUsers) throws IOException {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
|
||||
|
||||
if (allNumbers.isEmpty()) {
|
||||
Log.w(TAG, "No numbers to refresh!");
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch("refresh");
|
||||
|
||||
DirectoryResult result;
|
||||
|
||||
if (FeatureFlags.cds()) {
|
||||
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
|
||||
} else {
|
||||
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
|
||||
}
|
||||
|
||||
stopwatch.split("network");
|
||||
|
||||
if (result.getNumberRewrites().size() > 0) {
|
||||
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
|
||||
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
|
||||
}
|
||||
|
||||
Map<RecipientId, String> uuidMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
|
||||
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
|
||||
Set<RecipientId> activeIds = uuidMap.keySet();
|
||||
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
|
||||
.filterNot(activeNumbers::contains)
|
||||
.filterNot(n -> result.getNumberRewrites().containsKey(n))
|
||||
.map(recipientDatabase::getOrInsertFromE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
stopwatch.split("process-cds");
|
||||
|
||||
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
|
||||
|
||||
stopwatch.split("update-registered");
|
||||
|
||||
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
|
||||
|
||||
stopwatch.split("contacts-db");
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
|
||||
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
|
||||
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
|
||||
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
|
||||
|
||||
newlyActiveIds.removeAll(existingSignalIds);
|
||||
newlyActiveIds.retainAll(existingSystemIds);
|
||||
|
||||
notifyNewUsers(context, newlyActiveIds);
|
||||
} else {
|
||||
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
|
||||
}
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
}
|
||||
|
||||
|
||||
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
|
||||
try {
|
||||
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
|
||||
|
||||
@@ -41,6 +41,8 @@ import android.provider.Browser;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.Telephony;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
@@ -72,6 +74,7 @@ import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
@@ -111,6 +114,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||
@@ -124,7 +128,9 @@ import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -135,12 +141,14 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
@@ -149,6 +157,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeResult;
|
||||
@@ -158,6 +167,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
@@ -193,11 +203,11 @@ import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -217,6 +227,8 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
|
||||
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
@@ -224,9 +236,11 @@ import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
|
||||
@@ -243,10 +257,12 @@ import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -277,14 +293,15 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
AttachmentKeyboard.Callback,
|
||||
ConversationReactionOverlay.OnReactionSelectedListener,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
|
||||
SafetyNumberChangeDialog.Callback
|
||||
SafetyNumberChangeDialog.Callback,
|
||||
ReactionsBottomSheetDialogFragment.Callback
|
||||
{
|
||||
|
||||
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
|
||||
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String SAFETY_NUMBER_DIALOG = "SAFETY_NUMBER";
|
||||
private static final String STATE_REACT_WITH_ANY_PAGE = "STATE_REACT_WITH_ANY_PAGE";
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
@@ -339,6 +356,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private InputPanel inputPanel;
|
||||
private View panelParent;
|
||||
private View noLongerMemberBanner;
|
||||
private View requestingMemberBanner;
|
||||
private View cancelJoinRequest;
|
||||
private Stub<View> mentionsSuggestions;
|
||||
|
||||
private LinkPreviewViewModel linkPreviewViewModel;
|
||||
private ConversationSearchViewModel searchViewModel;
|
||||
@@ -350,6 +370,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
private int distributionType;
|
||||
private int reactWithAnyEmojiStartPage;
|
||||
private boolean isSecureText;
|
||||
private boolean isDefaultSms = true;
|
||||
private boolean isMmsEnabled = true;
|
||||
@@ -364,12 +385,21 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
long threadId,
|
||||
int distributionType,
|
||||
int startingPosition)
|
||||
{
|
||||
Intent intent = buildIntent(context, recipientId, threadId);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
|
||||
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static @NonNull Intent buildIntent(@NonNull Context context,
|
||||
@NonNull RecipientId recipientId,
|
||||
long threadId)
|
||||
{
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
|
||||
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
|
||||
|
||||
return intent;
|
||||
}
|
||||
@@ -412,6 +442,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
initializeStickerObserver();
|
||||
initializeViewModel();
|
||||
initializeGroupViewModel();
|
||||
if (FeatureFlags.mentions()) initializeMentionsViewModel();
|
||||
initializeEnabledCheck();
|
||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
@@ -452,6 +483,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
reactWithAnyEmojiStartPage = 0;
|
||||
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.getQuote().isPresent()) {
|
||||
saveDraft();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
@@ -504,11 +536,15 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
calculateCharactersRemaining();
|
||||
|
||||
if (recipientSnapshot.getGroupId().isPresent() && recipientSnapshot.getGroupId().get().isV2()) {
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(recipientSnapshot.getGroupId().get().requireV2()));
|
||||
GroupId.V2 groupId = recipientSnapshot.getGroupId().get().requireV2();
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(new RequestGroupV2InfoJob(groupId))
|
||||
.then(new GroupV2UpdateSelfProfileKeyJob(groupId))
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
markThreadAsRead();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -636,6 +672,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
boolean initiating = threadId == -1;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
|
||||
for (Media mediaItem : result.getNonUploadedMedia()) {
|
||||
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
@@ -657,6 +694,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
quote,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
mentions,
|
||||
expiresIn,
|
||||
result.isViewOnce(),
|
||||
subscriptionId,
|
||||
@@ -678,6 +716,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putInt(STATE_REACT_WITH_ANY_PAGE, reactWithAnyEmojiStartPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
|
||||
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, 0);
|
||||
}
|
||||
|
||||
private void handleImageFromDeviceCameraApp() {
|
||||
if (attachmentManager.getCaptureUri() == null) {
|
||||
Log.w(TAG, "No image available.");
|
||||
@@ -713,7 +765,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
@@ -732,7 +784,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
if (recipient != null && recipient.get().isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
|
||||
else inflater.inflate(R.menu.conversation_unmuted, menu);
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -868,7 +920,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
});
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1309,7 +1361,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void handleRecentSafetyNumberChange() {
|
||||
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
|
||||
records.addAll(identityRecords.getUntrustedRecords());
|
||||
SafetyNumberChangeDialog.create(records).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
SafetyNumberChangeDialog.show(getSupportFragmentManager(), records);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1330,6 +1382,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() { }
|
||||
|
||||
private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) {
|
||||
Log.i(TAG, "handleSecurityChange(" + isSecureText + ", " + isDefaultSms + ")");
|
||||
|
||||
@@ -1361,7 +1416,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private ListenableFuture<Boolean> initializeDraft() {
|
||||
final SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
|
||||
final String draftText = getIntent().getStringExtra(TEXT_EXTRA);
|
||||
final CharSequence draftText = getIntent().getCharSequenceExtra(TEXT_EXTRA);
|
||||
final Uri draftMedia = getIntent().getData();
|
||||
final String draftContentType = getIntent().getType();
|
||||
final MediaType draftMediaType = MediaType.from(draftContentType);
|
||||
@@ -1371,7 +1426,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (stickerLocator != null && draftMedia != null) {
|
||||
Log.d(TAG, "Handling shared sticker.");
|
||||
sendSticker(stickerLocator, draftMedia, 0, true);
|
||||
sendSticker(stickerLocator, Objects.requireNonNull(draftContentType), draftMedia, 0, true);
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
@@ -1411,33 +1466,98 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private void initializeEnabledCheck() {
|
||||
groupViewModel.getGroupActiveState().observe(this, state -> {
|
||||
boolean inactivePushGroup = state != null && isPushGroupConversation() && !state.isActiveGroup();
|
||||
boolean enabled = !inactivePushGroup;
|
||||
noLongerMemberBanner.setVisibility(enabled ? View.GONE : View.VISIBLE);
|
||||
inputPanel.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||
inputPanel.setEnabled(enabled);
|
||||
sendButton.setEnabled(enabled);
|
||||
attachButton.setEnabled(enabled);
|
||||
groupViewModel.getSelfMemberLevel().observe(this, selfMemberShip -> {
|
||||
boolean canSendMessages;
|
||||
boolean leftGroup;
|
||||
boolean canCancelRequest;
|
||||
|
||||
if (selfMemberShip == null) {
|
||||
leftGroup = false;
|
||||
canSendMessages = true;
|
||||
canCancelRequest = false;
|
||||
} else {
|
||||
switch (selfMemberShip) {
|
||||
case NOT_A_MEMBER:
|
||||
leftGroup = true;
|
||||
canSendMessages = false;
|
||||
canCancelRequest = false;
|
||||
break;
|
||||
case PENDING_MEMBER:
|
||||
leftGroup = false;
|
||||
canSendMessages = false;
|
||||
canCancelRequest = false;
|
||||
break;
|
||||
case REQUESTING_MEMBER:
|
||||
leftGroup = false;
|
||||
canSendMessages = false;
|
||||
canCancelRequest = true;
|
||||
break;
|
||||
case FULL_MEMBER:
|
||||
case ADMINISTRATOR:
|
||||
leftGroup = false;
|
||||
canSendMessages = true;
|
||||
canCancelRequest = false;
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
noLongerMemberBanner.setVisibility(leftGroup ? View.VISIBLE : View.GONE);
|
||||
requestingMemberBanner.setVisibility(canCancelRequest ? View.VISIBLE : View.GONE);
|
||||
if (canCancelRequest) {
|
||||
cancelJoinRequest.setOnClickListener(v -> ConversationGroupViewModel.onCancelJoinRequest(getRecipient(), new AsynchronousCallback.MainThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
Log.d(TAG, "Cancel request complete");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
Log.d(TAG, "Cancel join request failed " + error);
|
||||
Toast.makeText(ConversationActivity.this, GroupErrors.getUserDisplayMessage(error), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}.toWorkerCallback()));
|
||||
}
|
||||
|
||||
inputPanel.setVisibility(canSendMessages ? View.VISIBLE : View.GONE);
|
||||
inputPanel.setEnabled(canSendMessages);
|
||||
sendButton.setEnabled(canSendMessages);
|
||||
attachButton.setEnabled(canSendMessages);
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
new AsyncTask<Void, Void, List<Draft>>() {
|
||||
new AsyncTask<Void, Void, Pair<Drafts, CharSequence>>() {
|
||||
@Override
|
||||
protected List<Draft> doInBackground(Void... params) {
|
||||
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
|
||||
List<Draft> results = draftDatabase.getDrafts(threadId);
|
||||
protected Pair<Drafts, CharSequence> doInBackground(Void... params) {
|
||||
Context context = ConversationActivity.this;
|
||||
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(context);
|
||||
Drafts results = draftDatabase.getDrafts(threadId);
|
||||
Draft mentionsDraft = results.getDraftOfType(Draft.MENTION);
|
||||
Spannable updatedText = null;
|
||||
|
||||
if (mentionsDraft != null) {
|
||||
String text = results.getDraftOfType(Draft.TEXT).getValue();
|
||||
List<Mention> mentions = MentionUtil.bodyRangeListToMentions(context, Base64.decodeOrThrow(mentionsDraft.getValue()));
|
||||
UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, text, mentions);
|
||||
|
||||
updatedText = new SpannableString(updated.getBody());
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.getMentions());
|
||||
}
|
||||
|
||||
draftDatabase.clearDrafts(threadId);
|
||||
|
||||
return results;
|
||||
return new Pair<>(results, updatedText);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Draft> drafts) {
|
||||
protected void onPostExecute(Pair<Drafts, CharSequence> draftsWithUpdatedMentions) {
|
||||
Drafts drafts = Objects.requireNonNull(draftsWithUpdatedMentions.first());
|
||||
CharSequence updatedText = draftsWithUpdatedMentions.second();
|
||||
|
||||
if (drafts.isEmpty()) {
|
||||
future.set(false);
|
||||
updateToggleButtonState();
|
||||
@@ -1461,7 +1581,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
try {
|
||||
switch (draft.getType()) {
|
||||
case Draft.TEXT:
|
||||
composeText.setText(draft.getValue());
|
||||
composeText.setText(updatedText == null ? draft.getValue() : updatedText);
|
||||
listener.onSuccess(true);
|
||||
break;
|
||||
case Draft.LOCATION:
|
||||
@@ -1566,6 +1686,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reminderView.get().showReminder(new UnauthorizedReminder(this));
|
||||
} else if (ExpiredBuildReminder.isEligible()) {
|
||||
reminderView.get().showReminder(new ExpiredBuildReminder(this));
|
||||
reminderView.get().setOnActionClickListener(this::handleReminderAction);
|
||||
} else if (ServiceOutageReminder.isEligible(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
|
||||
reminderView.get().showReminder(new ServiceOutageReminder(this));
|
||||
@@ -1591,6 +1712,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
case R.id.reminder_action_view_insights:
|
||||
InsightsLauncher.showInsightsDashboard(getSupportFragmentManager());
|
||||
break;
|
||||
case R.id.reminder_action_update_now:
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown ID: " + reminderActionId);
|
||||
}
|
||||
@@ -1693,11 +1817,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
|
||||
messageRequestBottomView = ViewUtil.findById(this, R.id.conversation_activity_message_request_bottom_bar);
|
||||
reactionOverlay = ViewUtil.findById(this, R.id.conversation_reaction_scrubber);
|
||||
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
|
||||
|
||||
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
|
||||
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
|
||||
|
||||
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
|
||||
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
|
||||
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
|
||||
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
|
||||
|
||||
container.addOnKeyboardShownListener(this);
|
||||
inputPanel.setListener(this);
|
||||
@@ -1719,6 +1846,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
|
||||
calculateCharactersRemaining();
|
||||
updateLinkPreviewState();
|
||||
linkPreviewViewModel.onTransportChanged(newTransport.isSms());
|
||||
composeText.setTransport(newTransport);
|
||||
|
||||
buttonToggle.getBackground().setColorFilter(newTransport.getBackgroundColor(), PorterDuff.Mode.MULTIPLY);
|
||||
@@ -1781,17 +1909,15 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void initializeLinkPreviewObserver() {
|
||||
linkPreviewViewModel = ViewModelProviders.of(this, new LinkPreviewViewModel.Factory(new LinkPreviewRepository())).get(LinkPreviewViewModel.class);
|
||||
|
||||
if (!TextSecurePreferences.isLinkPreviewsEnabled(this)) {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
linkPreviewViewModel.getLinkPreviewState().observe(this, previewState -> {
|
||||
if (previewState == null) return;
|
||||
|
||||
if (previewState.isLoading()) {
|
||||
Log.d(TAG, "Loading link preview.");
|
||||
inputPanel.setLinkPreviewLoading();
|
||||
} else if (previewState.hasLinks() && !previewState.getLinkPreview().isPresent()) {
|
||||
Log.d(TAG, "No preview found.");
|
||||
inputPanel.setLinkPreviewNoPreview(previewState.getError());
|
||||
} else {
|
||||
Log.d(TAG, "Setting link preview: " + previewState.getLinkPreview().isPresent());
|
||||
inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview());
|
||||
@@ -1857,6 +1983,44 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
|
||||
}
|
||||
|
||||
private void initializeMentionsViewModel() {
|
||||
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||
|
||||
recipient.observe(this, r -> {
|
||||
if (r.isPushV2Group() && !mentionsSuggestions.resolved()) {
|
||||
mentionsSuggestions.get();
|
||||
}
|
||||
mentionsViewModel.onRecipientChange(r);
|
||||
});
|
||||
|
||||
composeText.setMentionQueryChangedListener(query -> {
|
||||
if (getRecipient().isPushV2Group()) {
|
||||
if (!mentionsSuggestions.resolved()) {
|
||||
mentionsSuggestions.get();
|
||||
}
|
||||
mentionsViewModel.onQueryChange(query);
|
||||
}
|
||||
});
|
||||
|
||||
composeText.setMentionValidator(annotations -> {
|
||||
if (!getRecipient().isPushV2Group()) {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
Set<String> validRecipientIds = Stream.of(getRecipient().getParticipants())
|
||||
.map(r -> MentionAnnotation.idToMentionAnnotationValue(r.getId()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return Stream.of(annotations)
|
||||
.filterNot(a -> validRecipientIds.contains(a.getValue()))
|
||||
.toList();
|
||||
});
|
||||
|
||||
mentionsViewModel.getSelectedRecipient().observe(this, recipient -> {
|
||||
composeText.replaceTextWithMention(recipient.getDisplayName(this), recipient.getId());
|
||||
});
|
||||
}
|
||||
|
||||
private void showStickerIntroductionTooltip() {
|
||||
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
|
||||
inputPanel.setMediaKeyboardToggleMode(true);
|
||||
@@ -1911,7 +2075,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
} else {
|
||||
reactionOverlay.hideAllButMask();
|
||||
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.createForMessageRecord(messageRecord)
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.createForMessageRecord(messageRecord, reactWithAnyEmojiStartPage)
|
||||
.show(getSupportFragmentManager(), "BOTTOM");
|
||||
}
|
||||
}
|
||||
@@ -1921,6 +2085,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reactionOverlay.hideMask();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactWithAnyEmojiPageChanged(int page) {
|
||||
reactWithAnyEmojiStartPage = page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchMoveUpPressed() {
|
||||
searchViewModel.onMoveUp();
|
||||
@@ -2038,7 +2207,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
boolean initiating = threadId == -1;
|
||||
|
||||
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), expiresIn, false, subscriptionId, initiating, false);
|
||||
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, false);
|
||||
}
|
||||
|
||||
private void selectContactInfo(ContactData contactData) {
|
||||
@@ -2062,7 +2231,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
Drafts drafts = new Drafts();
|
||||
|
||||
if (!Util.isEmpty(composeText)) {
|
||||
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed()));
|
||||
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed().toString()));
|
||||
List<Mention> draftMentions = composeText.getMentions();
|
||||
if (!draftMentions.isEmpty()) {
|
||||
drafts.add(new Draft(Draft.MENTION, Base64.encodeBytes(MentionUtil.mentionsToBodyRangeList(draftMentions).toByteArray())));
|
||||
}
|
||||
}
|
||||
|
||||
for (Slide slide : attachmentManager.buildSlideDeck().getSlides()) {
|
||||
@@ -2152,7 +2325,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private void calculateCharactersRemaining() {
|
||||
String messageBody = composeText.getTextTrimmed();
|
||||
String messageBody = composeText.getTextTrimmed().toString();
|
||||
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
|
||||
@@ -2235,7 +2408,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private String getMessage() throws InvalidMessageException {
|
||||
String rawText = composeText.getTextTrimmed();
|
||||
String rawText = composeText.getTextTrimmed().toString();
|
||||
|
||||
if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent())
|
||||
throw new InvalidMessageException(getString(R.string.ConversationActivity_message_is_empty_exclamation));
|
||||
@@ -2249,21 +2422,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
: MediaConstraints.getMmsMediaConstraints(sendButton.getSelectedTransport().getSimSubscriptionId().or(-1));
|
||||
}
|
||||
|
||||
private void markThreadAsRead() {
|
||||
new AsyncTask<Long, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Long... params) {
|
||||
Context context = ConversationActivity.this;
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false);
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
|
||||
}
|
||||
|
||||
private void markLastSeen() {
|
||||
new AsyncTask<Long, Void, Void>() {
|
||||
@Override
|
||||
@@ -2293,6 +2451,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
attachmentManager.cleanup();
|
||||
|
||||
updateLinkPreviewState();
|
||||
linkPreviewViewModel.onSend();
|
||||
}
|
||||
|
||||
private void sendMessage() {
|
||||
@@ -2319,6 +2478,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
recipient.isGroup() ||
|
||||
recipient.getEmail().isPresent() ||
|
||||
inputPanel.getQuote().isPresent() ||
|
||||
composeText.hasMentions() ||
|
||||
linkPreviewViewModel.hasLinkPreview() ||
|
||||
needsSplit;
|
||||
|
||||
@@ -2327,7 +2487,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
|
||||
handleManualMmsRequired();
|
||||
} else if (!forceSms && (identityRecords.isUnverified() || identityRecords.isUntrusted())) {
|
||||
} else if (!forceSms && (identityRecords.isUnverified(true) || identityRecords.isUntrusted(true))) {
|
||||
handleRecentSafetyNumberChange();
|
||||
} else if (isMediaMessage) {
|
||||
sendMediaMessage(forceSms, expiresIn, false, subscriptionId, initiating);
|
||||
@@ -2349,9 +2509,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void sendMediaMessage(@NonNull MediaSendActivityResult result) {
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
boolean initiating = threadId == -1;
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message );
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList(), mentions);
|
||||
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
|
||||
|
||||
ApplicationContext.getInstance(this).getTypingStatusSender().onTypingStopped(threadId);
|
||||
|
||||
@@ -2375,7 +2536,18 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
throws InvalidMessageException
|
||||
{
|
||||
Log.i(TAG, "Sending media message...");
|
||||
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), Collections.emptyList(), linkPreviewViewModel.getActiveLinkPreviews(), expiresIn, viewOnce, subscriptionId, initiating, true);
|
||||
sendMediaMessage(forceSms,
|
||||
getMessage(),
|
||||
attachmentManager.buildSlideDeck(),
|
||||
inputPanel.getQuote().orNull(),
|
||||
Collections.emptyList(),
|
||||
linkPreviewViewModel.getActiveLinkPreviews(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true);
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms,
|
||||
@@ -2384,6 +2556,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
QuoteModel quote,
|
||||
List<Contact> contacts,
|
||||
List<LinkPreview> previews,
|
||||
List<Mention> mentions,
|
||||
final long expiresIn,
|
||||
final boolean viewOnce,
|
||||
final int subscriptionId,
|
||||
@@ -2404,7 +2577,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient.get(), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews);
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient.get(), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, mentions);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = getApplicationContext();
|
||||
@@ -2512,7 +2685,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
buttonToggle.display(sendButton);
|
||||
quickAttachmentToggle.hide();
|
||||
|
||||
if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreview()) {
|
||||
if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreviewUi()) {
|
||||
inlineAttachmentToggle.show();
|
||||
} else {
|
||||
inlineAttachmentToggle.hide();
|
||||
@@ -2521,9 +2694,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private void updateLinkPreviewState() {
|
||||
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
|
||||
if (SignalStore.settings().isLinkPreviewsEnabled() && isSecureText && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
|
||||
linkPreviewViewModel.onEnabled();
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed().toString(), composeText.getSelectionStart(), composeText.getSelectionEnd());
|
||||
} else {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
}
|
||||
@@ -2591,7 +2764,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
sendMediaMessage(forceSms, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, true).addListener(new AssertedSuccessListener<Void>() {
|
||||
ListenableFuture<Void> sendResult = sendMediaMessage(forceSms,
|
||||
"",
|
||||
slideDeck,
|
||||
inputPanel.getQuote().orNull(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
false,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true);
|
||||
|
||||
sendResult.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void nothing) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@@ -2680,7 +2866,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onCursorPositionChanged(int start, int end) {
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), start, end);
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed().toString(), start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2695,7 +2881,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private void sendSticker(@NonNull StickerRecord stickerRecord, boolean clearCompose) {
|
||||
sendSticker(new StickerLocator(stickerRecord.getPackId(), stickerRecord.getPackKey(), stickerRecord.getStickerId()), stickerRecord.getUri(), stickerRecord.getSize(), clearCompose);
|
||||
sendSticker(new StickerLocator(stickerRecord.getPackId(), stickerRecord.getPackKey(), stickerRecord.getStickerId(), stickerRecord.getEmoji()), stickerRecord.getContentType(), stickerRecord.getUri(), stickerRecord.getSize(), clearCompose);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() ->
|
||||
DatabaseFactory.getStickerDatabase(getApplicationContext())
|
||||
@@ -2703,9 +2889,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
);
|
||||
}
|
||||
|
||||
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull Uri uri, long size, boolean clearCompose) {
|
||||
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull String contentType, @NonNull Uri uri, long size, boolean clearCompose) {
|
||||
if (sendButton.getSelectedTransport().isSms()) {
|
||||
Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, Optional.absent(), Optional.absent(), Optional.absent());
|
||||
Media media = new Media(uri, contentType, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, Optional.absent(), Optional.absent(), Optional.absent());
|
||||
Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
startActivityForResult(intent, MEDIA_SENDER);
|
||||
return;
|
||||
@@ -2716,11 +2902,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
boolean initiating = threadId == -1;
|
||||
TransportOption transport = sendButton.getSelectedTransport();
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
Slide stickerSlide = new StickerSlide(this, uri, size, stickerLocator);
|
||||
Slide stickerSlide = new StickerSlide(this, uri, size, stickerLocator, contentType);
|
||||
|
||||
slideDeck.addSlide(stickerSlide);
|
||||
|
||||
sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
|
||||
sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
|
||||
}
|
||||
|
||||
private void silentlySetComposeText(String text) {
|
||||
@@ -2729,6 +2915,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
typingTextWatcher.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactionsDialogDismissed() {
|
||||
reactionOverlay.hideMask();
|
||||
}
|
||||
|
||||
// Listeners
|
||||
|
||||
private class QuickCameraToggleListener implements OnClickListener {
|
||||
@@ -2893,7 +3084,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
{
|
||||
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionOverlay.setOnHideListener(onHideListener);
|
||||
reactionOverlay.show(this, maskTarget, messageRecord, panelParent.getMeasuredHeight());
|
||||
reactionOverlay.show(this, maskTarget, messageRecord, inputAreaHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2910,12 +3101,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
.setPositiveButton(R.string.conversation_activity__send, (dialog, which) -> MessageSender.resend(this, messageRecord))
|
||||
.show();
|
||||
} else if (messageRecord.isIdentityMismatchFailure()) {
|
||||
SafetyNumberChangeDialog.create(this, messageRecord).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
SafetyNumberChangeDialog.show(this, messageRecord);
|
||||
} else {
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(this, messageRecord, messageRecord.getRecipient().getId(), messageRecord.getThreadId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReactionDetails(@NonNull View maskTarget) {
|
||||
reactionOverlay.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorChanged() {
|
||||
if (!reactionOverlay.isShowing()) {
|
||||
@@ -2939,7 +3135,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReplyMessage(MessageRecord messageRecord) {
|
||||
public void handleReplyMessage(ConversationMessage conversationMessage) {
|
||||
MessageRecord messageRecord = conversationMessage.getMessageRecord();
|
||||
|
||||
Recipient author;
|
||||
|
||||
if (messageRecord.isOutgoing()) {
|
||||
@@ -2975,7 +3173,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
conversationMessage.getDisplayBody(this),
|
||||
slideDeck);
|
||||
} else {
|
||||
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
|
||||
@@ -2989,7 +3187,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
conversationMessage.getDisplayBody(this),
|
||||
slideDeck);
|
||||
}
|
||||
|
||||
@@ -3013,6 +3211,19 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
updateLinkPreviewState();
|
||||
}
|
||||
|
||||
private int inputAreaHeight() {
|
||||
int height = panelParent.getMeasuredHeight();
|
||||
|
||||
if (attachmentKeyboardStub.resolved()) {
|
||||
View keyboard = attachmentKeyboardStub.get();
|
||||
if (keyboard.getVisibility() == View.VISIBLE) {
|
||||
return height + keyboard.getMeasuredHeight();
|
||||
}
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
private void onMessageRequestDeleteClicked(@NonNull MessageRequestViewModel requestModel) {
|
||||
Recipient recipient = requestModel.getRecipient().getValue();
|
||||
if (recipient == null) {
|
||||
@@ -3143,6 +3354,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
quote,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
false,
|
||||
subscriptionId,
|
||||
@@ -3201,7 +3413,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
private class QuoteRestorationTask extends AsyncTask<Void, Void, MessageRecord> {
|
||||
private class QuoteRestorationTask extends AsyncTask<Void, Void, ConversationMessage> {
|
||||
|
||||
private final String serialized;
|
||||
private final SettableFuture<Boolean> future;
|
||||
@@ -3212,20 +3424,27 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageRecord doInBackground(Void... voids) {
|
||||
protected ConversationMessage doInBackground(Void... voids) {
|
||||
QuoteId quoteId = QuoteId.deserialize(ConversationActivity.this, serialized);
|
||||
|
||||
if (quoteId != null) {
|
||||
return DatabaseFactory.getMmsSmsDatabase(getApplicationContext()).getMessageFor(quoteId.getId(), quoteId.getAuthor());
|
||||
if (quoteId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
Context context = getApplicationContext();
|
||||
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteId.getId(), quoteId.getAuthor());
|
||||
if (messageRecord == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(MessageRecord messageRecord) {
|
||||
if (messageRecord != null) {
|
||||
handleReplyMessage(messageRecord);
|
||||
protected void onPostExecute(ConversationMessage conversationMessage) {
|
||||
if (conversationMessage != null) {
|
||||
handleReplyMessage(conversationMessage);
|
||||
future.set(true);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to restore a quote from a draft. No matching message record.");
|
||||
|
||||
@@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -65,8 +64,8 @@ import java.util.Set;
|
||||
* manager, so position 0 is at the bottom of the screen. That's why the "header" is at the bottom,
|
||||
* the "footer" is at the top, and we refer to the "next" record as having a lower index.
|
||||
*/
|
||||
public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
extends PagedListAdapter<MessageRecord, RecyclerView.ViewHolder>
|
||||
public class ConversationAdapter
|
||||
extends PagedListAdapter<ConversationMessage, RecyclerView.ViewHolder>
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>
|
||||
{
|
||||
|
||||
@@ -89,16 +88,16 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
private final Locale locale;
|
||||
private final Recipient recipient;
|
||||
|
||||
private final Set<MessageRecord> selected;
|
||||
private final List<MessageRecord> fastRecords;
|
||||
private final Set<Long> releasedFastRecords;
|
||||
private final Calendar calendar;
|
||||
private final MessageDigest digest;
|
||||
private final Set<ConversationMessage> selected;
|
||||
private final List<ConversationMessage> fastRecords;
|
||||
private final Set<Long> releasedFastRecords;
|
||||
private final Calendar calendar;
|
||||
private final MessageDigest digest;
|
||||
|
||||
private String searchQuery;
|
||||
private MessageRecord recordToPulseHighlight;
|
||||
private View headerView;
|
||||
private View footerView;
|
||||
private String searchQuery;
|
||||
private ConversationMessage recordToPulse;
|
||||
private View headerView;
|
||||
private View footerView;
|
||||
|
||||
ConversationAdapter(@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@@ -130,7 +129,8 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
return MESSAGE_TYPE_FOOTER;
|
||||
}
|
||||
|
||||
MessageRecord messageRecord = getItem(position);
|
||||
ConversationMessage conversationMessage = getItem(position);
|
||||
MessageRecord messageRecord = (conversationMessage != null) ? conversationMessage.getMessageRecord() : null;
|
||||
|
||||
if (messageRecord == null) {
|
||||
return MESSAGE_TYPE_PLACEHOLDER;
|
||||
@@ -153,16 +153,13 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
return FOOTER_ID;
|
||||
}
|
||||
|
||||
MessageRecord record = getItem(position);
|
||||
ConversationMessage message = getItem(position);
|
||||
|
||||
if (record == null) {
|
||||
if (message == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
String unique = (record.isMms() ? "MMS::" : "SMS::") + record.getId();
|
||||
byte[] bytes = digest.digest(unique.getBytes());
|
||||
|
||||
return Conversions.byteArrayToLong(bytes);
|
||||
return message.getUniqueId(digest);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -175,22 +172,23 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
V itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
|
||||
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
|
||||
BindableConversationItem bindable = (BindableConversationItem) itemView;
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemClick(itemView.getMessageRecord());
|
||||
clickListener.onItemClick(bindable.getConversationMessage());
|
||||
}
|
||||
});
|
||||
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemLongClick(itemView, itemView.getMessageRecord());
|
||||
clickListener.onItemLongClick(itemView, bindable.getConversationMessage());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
itemView.setEventListener(clickListener);
|
||||
bindable.setEventListener(clickListener);
|
||||
|
||||
Log.d(TAG, String.format(Locale.US, "Inflate time: %d ms for View type: %d", System.currentTimeMillis() - start, viewType));
|
||||
return new ConversationViewHolder(itemView);
|
||||
@@ -215,24 +213,24 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder;
|
||||
MessageRecord messageRecord = Objects.requireNonNull(getItem(position));
|
||||
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
||||
int adapterPosition = holder.getAdapterPosition();
|
||||
|
||||
MessageRecord previousRecord = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
||||
MessageRecord nextRecord = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
||||
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
||||
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
||||
|
||||
conversationViewHolder.getView().bind(messageRecord,
|
||||
Optional.fromNullable(previousRecord),
|
||||
Optional.fromNullable(nextRecord),
|
||||
glideRequests,
|
||||
locale,
|
||||
selected,
|
||||
recipient,
|
||||
searchQuery,
|
||||
messageRecord == recordToPulseHighlight);
|
||||
conversationViewHolder.getBindable().bind(conversationMessage,
|
||||
Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null),
|
||||
Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null),
|
||||
glideRequests,
|
||||
locale,
|
||||
selected,
|
||||
recipient,
|
||||
searchQuery,
|
||||
conversationMessage == recordToPulse);
|
||||
|
||||
if (messageRecord == recordToPulseHighlight) {
|
||||
recordToPulseHighlight = null;
|
||||
if (conversationMessage == recordToPulse) {
|
||||
recordToPulse = null;
|
||||
}
|
||||
break;
|
||||
case MESSAGE_TYPE_HEADER:
|
||||
@@ -245,16 +243,18 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submitList(@Nullable PagedList<MessageRecord> pagedList) {
|
||||
public void submitList(@Nullable PagedList<ConversationMessage> pagedList) {
|
||||
cleanFastRecords();
|
||||
super.submitList(pagedList);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable MessageRecord getItem(int position) {
|
||||
protected @Nullable ConversationMessage getItem(int position) {
|
||||
position = hasHeader() ? position - 1 : position;
|
||||
|
||||
if (position < fastRecords.size()) {
|
||||
if (position == -1) {
|
||||
return null;
|
||||
} else if (position < fastRecords.size()) {
|
||||
return fastRecords.get(position);
|
||||
} else {
|
||||
int correctedPosition = position - fastRecords.size();
|
||||
@@ -272,7 +272,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof ConversationViewHolder) {
|
||||
((ConversationViewHolder) holder).getView().unbind();
|
||||
((ConversationViewHolder) holder).getBindable().unbind();
|
||||
} else if (holder instanceof HeaderFooterViewHolder) {
|
||||
((HeaderFooterViewHolder) holder).unbind();
|
||||
}
|
||||
@@ -285,11 +285,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
if (position >= getItemCount()) return -1;
|
||||
if (position < 0) return -1;
|
||||
|
||||
MessageRecord record = getItem(position);
|
||||
ConversationMessage conversationMessage = getItem(position);
|
||||
|
||||
if (record == null) return -1;
|
||||
if (conversationMessage == null) return -1;
|
||||
|
||||
calendar.setTime(new Date(record.getDateSent()));
|
||||
calendar.setTime(new Date(conversationMessage.getMessageRecord().getDateSent()));
|
||||
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
|
||||
}
|
||||
|
||||
@@ -300,14 +300,18 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position) {
|
||||
MessageRecord messageRecord = Objects.requireNonNull(getItem(position));
|
||||
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, messageRecord.getDateReceived()));
|
||||
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
||||
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived()));
|
||||
}
|
||||
|
||||
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
|
||||
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
|
||||
}
|
||||
|
||||
boolean hasNoConversationMessages() {
|
||||
return super.getItemCount() + fastRecords.size() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The presence of a header may throw off the position you'd like to jump to. This will return
|
||||
* an adjusted message position based on adapter state.
|
||||
@@ -328,12 +332,12 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
if (position >= getItemCount()) return 0;
|
||||
if (position < 0) return 0;
|
||||
|
||||
MessageRecord messageRecord = getItem(position);
|
||||
ConversationMessage conversationMessage = getItem(position);
|
||||
|
||||
if (messageRecord == null || messageRecord.isOutgoing()) {
|
||||
if (conversationMessage == null || conversationMessage.getMessageRecord().isOutgoing()) {
|
||||
return 0;
|
||||
} else {
|
||||
return messageRecord.getDateReceived();
|
||||
return conversationMessage.getMessageRecord().getDateReceived();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,13 +383,13 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
}
|
||||
|
||||
/**
|
||||
* Momentarily highlights a row at the requested position.
|
||||
* Momentarily highlights a mention at the requested position.
|
||||
*/
|
||||
void pulseHighlightItem(int position) {
|
||||
void pulseAtPosition(int position) {
|
||||
if (position >= 0 && position < getItemCount()) {
|
||||
int correctedPosition = isHeaderPosition(position) ? position + 1 : position;
|
||||
|
||||
recordToPulseHighlight = getItem(correctedPosition);
|
||||
recordToPulse = getItem(correctedPosition);
|
||||
notifyItemChanged(correctedPosition);
|
||||
}
|
||||
}
|
||||
@@ -403,8 +407,8 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
* for a database change.
|
||||
*/
|
||||
@MainThread
|
||||
void addFastRecord(MessageRecord record) {
|
||||
fastRecords.add(0, record);
|
||||
void addFastRecord(ConversationMessage conversationMessage) {
|
||||
fastRecords.add(0, conversationMessage);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -422,7 +426,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
/**
|
||||
* Returns set of records that are selected in multi-select mode.
|
||||
*/
|
||||
Set<MessageRecord> getSelectedItems() {
|
||||
Set<ConversationMessage> getSelectedItems() {
|
||||
return new HashSet<>(selected);
|
||||
}
|
||||
|
||||
@@ -436,11 +440,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
/**
|
||||
* Toggles the selected state of a record in multi-select mode.
|
||||
*/
|
||||
void toggleSelection(MessageRecord record) {
|
||||
if (selected.contains(record)) {
|
||||
selected.remove(record);
|
||||
void toggleSelection(ConversationMessage conversationMessage) {
|
||||
if (selected.contains(conversationMessage)) {
|
||||
selected.remove(conversationMessage);
|
||||
} else {
|
||||
selected.add(record);
|
||||
selected.add(conversationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,11 +468,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
Util.assertMainThread();
|
||||
|
||||
synchronized (releasedFastRecords) {
|
||||
Iterator<MessageRecord> recordIterator = fastRecords.iterator();
|
||||
while (recordIterator.hasNext()) {
|
||||
long id = recordIterator.next().getId();
|
||||
Iterator<ConversationMessage> messageIterator = fastRecords.iterator();
|
||||
while (messageIterator.hasNext()) {
|
||||
long id = messageIterator.next().getMessageRecord().getId();
|
||||
if (releasedFastRecords.contains(id)) {
|
||||
recordIterator.remove();
|
||||
messageIterator.remove();
|
||||
releasedFastRecords.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -510,18 +514,17 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getLastVisibleMessageRecord(int position) {
|
||||
public @Nullable ConversationMessage getLastVisibleConversationMessage(int position) {
|
||||
return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0));
|
||||
}
|
||||
|
||||
static class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationItem> ConversationViewHolder(final @NonNull V itemView) {
|
||||
public ConversationViewHolder(final @NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public <V extends View & BindableConversationItem> V getView() {
|
||||
//noinspection unchecked
|
||||
return (V)itemView;
|
||||
public BindableConversationItem getBindable() {
|
||||
return (BindableConversationItem) itemView;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +533,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
|
||||
StickyHeaderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = ViewUtil.findById(itemView, R.id.text);
|
||||
textView = itemView.findViewById(R.id.text);
|
||||
}
|
||||
|
||||
StickyHeaderViewHolder(TextView textView) {
|
||||
@@ -571,21 +574,21 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiffCallback extends DiffUtil.ItemCallback<MessageRecord> {
|
||||
private static class DiffCallback extends DiffUtil.ItemCallback<ConversationMessage> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull MessageRecord oldItem, @NonNull MessageRecord newItem) {
|
||||
return oldItem.isMms() == newItem.isMms() && oldItem.getId() == newItem.getId();
|
||||
public boolean areItemsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
|
||||
return oldItem.getMessageRecord().isMms() == newItem.getMessageRecord().isMms() && oldItem.getMessageRecord().getId() == newItem.getMessageRecord().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull MessageRecord oldItem, @NonNull MessageRecord newItem) {
|
||||
public boolean areContentsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
|
||||
// Corner rounding is not part of the model, so we can't use this yet
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||
void onItemClick(MessageRecord item);
|
||||
void onItemLongClick(View maskTarget, MessageRecord item);
|
||||
void onItemClick(ConversationMessage item);
|
||||
void onItemLongClick(View maskTarget, ConversationMessage item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
@@ -54,6 +55,7 @@ public class ConversationBannerView extends ConstraintLayout {
|
||||
|
||||
public void setSubtitle(@Nullable CharSequence subtitle) {
|
||||
contactSubtitle.setText(subtitle);
|
||||
contactSubtitle.setVisibility(TextUtils.isEmpty(subtitle) ? GONE : VISIBLE);
|
||||
}
|
||||
|
||||
public void setDescription(@Nullable CharSequence description) {
|
||||
|
||||
@@ -4,27 +4,35 @@ import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.paging.PositionalDataSource;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Core data source for loading an individual conversation.
|
||||
*/
|
||||
class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationDataSource.class);
|
||||
|
||||
@@ -57,7 +65,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<MessageRecord> callback) {
|
||||
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<ConversationMessage> callback) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
@@ -65,43 +73,80 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
int totalCount = db.getConversationCount(threadId);
|
||||
int effectiveCount = params.requestedStartPosition;
|
||||
|
||||
MentionHelper mentionHelper = new MentionHelper();
|
||||
|
||||
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.requestedStartPosition, params.requestedLoadSize))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
|
||||
records.add(record);
|
||||
mentionHelper.add(record);
|
||||
effectiveCount++;
|
||||
}
|
||||
}
|
||||
|
||||
mentionHelper.fetchMentions(context);
|
||||
|
||||
if (!isInvalid()) {
|
||||
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
|
||||
|
||||
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
|
||||
}
|
||||
List<ConversationMessage> items = Stream.of(result.getItems())
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
|
||||
.toList();
|
||||
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", size: " + params.requestedLoadSize + (isInvalid() ? " -- invalidated" : ""));
|
||||
callback.onResult(items, params.requestedStartPosition, result.getTotal());
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", actualSize: " + result.getItems().size() + ", totalCount: " + result.getTotal());
|
||||
} else {
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", totalCount: " + totalCount + " -- invalidated");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<MessageRecord> callback) {
|
||||
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<ConversationMessage> callback) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(params.loadSize);
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(params.loadSize);
|
||||
MentionHelper mentionHelper = new MentionHelper();
|
||||
|
||||
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.startPosition, params.loadSize))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||
records.add(record);
|
||||
mentionHelper.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
callback.onResult(records);
|
||||
mentionHelper.fetchMentions(context);
|
||||
|
||||
List<ConversationMessage> items = Stream.of(records)
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
|
||||
.toList();
|
||||
callback.onResult(items);
|
||||
|
||||
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
static class Factory extends DataSource.Factory<Integer, MessageRecord> {
|
||||
private static class MentionHelper {
|
||||
|
||||
private Collection<Long> messageIds = new LinkedList<>();
|
||||
private Map<Long, List<Mention>> messageIdToMentions = new HashMap<>();
|
||||
|
||||
void add(MessageRecord record) {
|
||||
if (record.isMms()) {
|
||||
messageIds.add(record.getId());
|
||||
}
|
||||
}
|
||||
|
||||
void fetchMentions(Context context) {
|
||||
messageIdToMentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessages(messageIds);
|
||||
}
|
||||
|
||||
@Nullable List<Mention> getMentions(long id) {
|
||||
return messageIdToMentions.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
static class Factory extends DataSource.Factory<Integer, ConversationMessage> {
|
||||
|
||||
private final Context context;
|
||||
private final long threadId;
|
||||
@@ -114,7 +159,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource<Integer, MessageRecord> create() {
|
||||
public @NonNull DataSource<Integer, ConversationMessage> create() {
|
||||
return new ConversationDataSource(context, threadId, invalidator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
@@ -25,7 +27,7 @@ import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.ClipboardManager;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -55,6 +57,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
@@ -63,6 +66,7 @@ import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.ConversationScrollToView;
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||
@@ -71,9 +75,12 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
@@ -127,8 +134,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
@@ -152,18 +157,28 @@ public class ConversationFragment extends LoggingFragment {
|
||||
private Locale locale;
|
||||
private RecyclerView list;
|
||||
private RecyclerView.ItemDecoration lastSeenDecoration;
|
||||
private RecyclerView.ItemDecoration stickyHeaderDecoration;
|
||||
private ViewSwitcher topLoadMoreView;
|
||||
private ViewSwitcher bottomLoadMoreView;
|
||||
private ConversationTypingView typingView;
|
||||
private UnknownSenderView unknownSenderView;
|
||||
private View composeDivider;
|
||||
private View scrollToBottomButton;
|
||||
private ConversationScrollToView scrollToBottomButton;
|
||||
private ConversationScrollToView scrollToMentionButton;
|
||||
private TextView scrollDateHeader;
|
||||
private ConversationBannerView conversationBanner;
|
||||
private ConversationBannerView emptyConversationBanner;
|
||||
private MessageRequestViewModel messageRequestViewModel;
|
||||
private MessageCountsViewModel messageCountsViewModel;
|
||||
private ConversationViewModel conversationViewModel;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private MarkReadHelper markReadHelper;
|
||||
private Animation scrollButtonInAnimation;
|
||||
private Animation mentionButtonInAnimation;
|
||||
private Animation scrollButtonOutAnimation;
|
||||
private Animation mentionButtonOutAnimation;
|
||||
private OnScrollListener conversationScrollListener;
|
||||
private int pulsePosition = -1;
|
||||
|
||||
public static void prepare(@NonNull Context context) {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
@@ -186,13 +201,13 @@ public class ConversationFragment extends LoggingFragment {
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
||||
list = ViewUtil.findById(view, android.R.id.list);
|
||||
composeDivider = ViewUtil.findById(view, R.id.compose_divider);
|
||||
scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button);
|
||||
scrollDateHeader = ViewUtil.findById(view, R.id.scroll_date_header);
|
||||
emptyConversationBanner = ViewUtil.findById(view, R.id.empty_conversation_banner);
|
||||
list = view.findViewById(android.R.id.list);
|
||||
composeDivider = view.findViewById(R.id.compose_divider);
|
||||
|
||||
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
|
||||
scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
|
||||
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
|
||||
scrollDateHeader = view.findViewById(R.id.scroll_date_header);
|
||||
emptyConversationBanner = view.findViewById(R.id.empty_conversation_banner);
|
||||
|
||||
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
|
||||
list.setHasFixedSize(false);
|
||||
@@ -210,14 +225,16 @@ public class ConversationFragment extends LoggingFragment {
|
||||
typingView = (ConversationTypingView) inflater.inflate(R.layout.conversation_typing_view, container, false);
|
||||
|
||||
new ConversationItemSwipeCallback(
|
||||
messageRecord -> actionMode == null &&
|
||||
MenuState.canReplyToMessage(MenuState.isActionMessage(messageRecord), messageRecord, messageRequestViewModel.shouldShowMessageRequest()),
|
||||
conversationMessage -> actionMode == null &&
|
||||
MenuState.canReplyToMessage(MenuState.isActionMessage(conversationMessage.getMessageRecord()), conversationMessage.getMessageRecord(), messageRequestViewModel.shouldShowMessageRequest()),
|
||||
this::handleReplyMessage
|
||||
).attachToRecyclerView(list);
|
||||
|
||||
setupListLayoutListeners();
|
||||
|
||||
this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
||||
this.messageCountsViewModel = ViewModelProviders.of(requireActivity()).get(MessageCountsViewModel.class);
|
||||
this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
||||
|
||||
conversationViewModel.getMessages().observe(this, list -> {
|
||||
if (getListAdapter() != null && !list.getDataSource().isInvalid()) {
|
||||
Log.i(TAG, "submitList");
|
||||
@@ -228,6 +245,25 @@ public class ConversationFragment extends LoggingFragment {
|
||||
});
|
||||
conversationViewModel.getConversationMetadata().observe(this, this::presentConversationMetadata);
|
||||
|
||||
conversationViewModel.getShowMentionsButton().observe(this, shouldShow -> {
|
||||
if (shouldShow) {
|
||||
ViewUtil.animateIn(scrollToMentionButton, mentionButtonInAnimation);
|
||||
} else {
|
||||
ViewUtil.animateOut(scrollToMentionButton, mentionButtonOutAnimation, View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
conversationViewModel.getShowScrollToBottom().observe(this, shouldShow -> {
|
||||
if (shouldShow) {
|
||||
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
|
||||
} else {
|
||||
ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
|
||||
scrollToMentionButton.setOnClickListener(v -> scrollToNextMention());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -263,6 +299,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
|
||||
initializeScrollButtonAnimations();
|
||||
initializeResources();
|
||||
initializeMessageRequestViewModel();
|
||||
initializeListAdapter();
|
||||
@@ -288,9 +325,9 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
final long lastVisibleMessageTimestamp;
|
||||
if (firstVisiblePosition > 0 && lastVisiblePosition != RecyclerView.NO_POSITION) {
|
||||
MessageRecord message = getListAdapter().getLastVisibleMessageRecord(lastVisiblePosition);
|
||||
ConversationMessage message = getListAdapter().getLastVisibleConversationMessage(lastVisiblePosition);
|
||||
|
||||
lastVisibleMessageTimestamp = message != null ? message.getDateReceived() : 0;
|
||||
lastVisibleMessageTimestamp = message != null ? message.getMessageRecord().getDateReceived() : 0;
|
||||
} else {
|
||||
lastVisibleMessageTimestamp = 0;
|
||||
}
|
||||
@@ -357,7 +394,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
if (recipient != null) {
|
||||
conversationBanner.setAvatar(GlideApp.with(context), recipient);
|
||||
|
||||
String title = isSelf ? context.getString(R.string.note_to_self) : recipient.getDisplayName(context);
|
||||
String title = isSelf ? context.getString(R.string.note_to_self) : recipient.getDisplayNameOrUsername(context);
|
||||
conversationBanner.setTitle(title);
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
@@ -365,14 +402,16 @@ public class ConversationFragment extends LoggingFragment {
|
||||
conversationBanner.setSubtitle(context.getResources()
|
||||
.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, memberCount,
|
||||
memberCount, pendingMemberCount));
|
||||
} else {
|
||||
} else if (memberCount > 0) {
|
||||
conversationBanner.setSubtitle(context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_members, memberCount,
|
||||
memberCount));
|
||||
} else {
|
||||
conversationBanner.setSubtitle(null);
|
||||
}
|
||||
} else if (isSelf) {
|
||||
conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation));
|
||||
} else {
|
||||
String subtitle = recipient.getUsername().or(recipient.getE164()).orNull();
|
||||
String subtitle = recipient.getE164().orNull();
|
||||
|
||||
if (subtitle == null || subtitle.equals(title)) {
|
||||
conversationBanner.hideSubtitle();
|
||||
@@ -402,7 +441,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
description = context.getString(R.string.MessageRequestProfileView_member_of_many_groups,
|
||||
HtmlUtil.bold(groups.get(0)),
|
||||
HtmlUtil.bold(groups.get(1)),
|
||||
context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_member_of_others, others, others));
|
||||
context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, others, others));
|
||||
}
|
||||
|
||||
conversationBanner.setDescription(HtmlCompat.fromHtml(description, 0));
|
||||
@@ -418,11 +457,19 @@ public class ConversationFragment extends LoggingFragment {
|
||||
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
|
||||
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
|
||||
this.unknownSenderView = new UnknownSenderView(getActivity(), recipient.get(), threadId, () -> clearHeaderIfNotTyping(getListAdapter()));
|
||||
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
|
||||
|
||||
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
|
||||
messageCountsViewModel.setThreadId(threadId);
|
||||
|
||||
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
|
||||
list.addOnScrollListener(scrollListener);
|
||||
messageCountsViewModel.getUnreadMessagesCount().observe(getViewLifecycleOwner(), scrollToBottomButton::setUnreadCount);
|
||||
messageCountsViewModel.getUnreadMentionsCount().observe(getViewLifecycleOwner(), count -> {
|
||||
scrollToMentionButton.setUnreadCount(count);
|
||||
conversationViewModel.setHasUnreadMentions(count > 0);
|
||||
});
|
||||
|
||||
conversationScrollListener = new ConversationScrollListener(requireContext());
|
||||
list.addOnScrollListener(conversationScrollListener);
|
||||
|
||||
if (oldThreadId != threadId) {
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypists(oldThreadId).removeObservers(this);
|
||||
@@ -434,7 +481,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
Log.d(TAG, "Initializing adapter for " + recipient.getId());
|
||||
ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
|
||||
setStickyHeaderDecoration(adapter);
|
||||
ConversationAdapter.initializePool(list.getRecycledViewPool());
|
||||
|
||||
adapter.registerAdapterDataObserver(snapToTopDataObserver);
|
||||
@@ -519,14 +566,14 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
private void setCorrectMenuVisibility(@NonNull Menu menu) {
|
||||
Set<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
|
||||
Set<ConversationMessage> messages = getListAdapter().getSelectedItems();
|
||||
|
||||
if (actionMode != null && messageRecords.size() == 0) {
|
||||
if (actionMode != null && messages.size() == 0) {
|
||||
actionMode.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
MenuState menuState = MenuState.getMenuState(messageRecords, messageRequestViewModel.shouldShowMessageRequest());
|
||||
MenuState menuState = MenuState.getMenuState(Stream.of(messages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()), messageRequestViewModel.shouldShowMessageRequest());
|
||||
|
||||
menu.findItem(R.id.menu_context_forward).setVisible(menuState.shouldShowForwardAction());
|
||||
menu.findItem(R.id.menu_context_reply).setVisible(menuState.shouldShowReplyAction());
|
||||
@@ -544,8 +591,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
return (SmoothScrollingLinearLayoutManager) list.getLayoutManager();
|
||||
}
|
||||
|
||||
private MessageRecord getSelectedMessageRecord() {
|
||||
Set<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
|
||||
private ConversationMessage getSelectedConversationMessage() {
|
||||
Set<ConversationMessage> messageRecords = getListAdapter().getSelectedItems();
|
||||
|
||||
if (messageRecords.size() == 1) return messageRecords.iterator().next();
|
||||
else throw new AssertionError();
|
||||
@@ -560,6 +607,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
snapToTopDataObserver.requestScrollPosition(0);
|
||||
conversationViewModel.onConversationDataAvailable(threadId, -1);
|
||||
messageCountsViewModel.setThreadId(threadId);
|
||||
initializeListAdapter();
|
||||
}
|
||||
}
|
||||
@@ -572,6 +620,15 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public void setStickyHeaderDecoration(@NonNull ConversationAdapter adapter) {
|
||||
if (stickyHeaderDecoration != null) {
|
||||
list.removeItemDecoration(stickyHeaderDecoration);
|
||||
}
|
||||
|
||||
stickyHeaderDecoration = new StickyHeaderDecoration(adapter, false, false);
|
||||
list.addItemDecoration(stickyHeaderDecoration);
|
||||
}
|
||||
|
||||
public void setLastSeen(long lastSeen) {
|
||||
if (lastSeenDecoration != null) {
|
||||
list.removeItemDecoration(lastSeenDecoration);
|
||||
@@ -581,37 +638,30 @@ public class ConversationFragment extends LoggingFragment {
|
||||
list.addItemDecoration(lastSeenDecoration);
|
||||
}
|
||||
|
||||
private void handleCopyMessage(final Set<MessageRecord> messageRecords) {
|
||||
List<MessageRecord> messageList = new LinkedList<>(messageRecords);
|
||||
Collections.sort(messageList, new Comparator<MessageRecord>() {
|
||||
@Override
|
||||
public int compare(MessageRecord lhs, MessageRecord rhs) {
|
||||
if (lhs.getDateReceived() < rhs.getDateReceived()) return -1;
|
||||
else if (lhs.getDateReceived() == rhs.getDateReceived()) return 0;
|
||||
else return 1;
|
||||
}
|
||||
});
|
||||
private void handleCopyMessage(final Set<ConversationMessage> conversationMessages) {
|
||||
List<ConversationMessage> messageList = new ArrayList<>(conversationMessages);
|
||||
Collections.sort(messageList, (lhs, rhs) -> Long.compare(lhs.getMessageRecord().getDateReceived(), rhs.getMessageRecord().getDateReceived()));
|
||||
|
||||
StringBuilder bodyBuilder = new StringBuilder();
|
||||
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
SpannableStringBuilder bodyBuilder = new SpannableStringBuilder();
|
||||
ClipboardManager clipboard = (ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
for (MessageRecord messageRecord : messageList) {
|
||||
String body = messageRecord.getDisplayBody(requireContext()).toString();
|
||||
for (ConversationMessage message : messageList) {
|
||||
CharSequence body = message.getDisplayBody(requireContext());
|
||||
if (!TextUtils.isEmpty(body)) {
|
||||
bodyBuilder.append(body).append('\n');
|
||||
if (bodyBuilder.length() > 0) {
|
||||
bodyBuilder.append('\n');
|
||||
}
|
||||
bodyBuilder.append(body);
|
||||
}
|
||||
}
|
||||
if (bodyBuilder.length() > 0 && bodyBuilder.charAt(bodyBuilder.length() - 1) == '\n') {
|
||||
bodyBuilder.deleteCharAt(bodyBuilder.length() - 1);
|
||||
|
||||
if (!TextUtils.isEmpty(bodyBuilder)) {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, bodyBuilder));
|
||||
}
|
||||
|
||||
String result = bodyBuilder.toString();
|
||||
|
||||
if (!TextUtils.isEmpty(result))
|
||||
clipboard.setText(result);
|
||||
}
|
||||
|
||||
private void handleDeleteMessages(final Set<MessageRecord> messageRecords) {
|
||||
private void handleDeleteMessages(final Set<ConversationMessage> conversationMessages) {
|
||||
Set<MessageRecord> messageRecords = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet());
|
||||
if (FeatureFlags.remoteDelete()) {
|
||||
buildRemoteDeleteConfirmationDialog(messageRecords).show();
|
||||
} else {
|
||||
@@ -639,13 +689,15 @@ public class ConversationFragment extends LoggingFragment {
|
||||
boolean threadDeleted;
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
threadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
||||
threadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
|
||||
} else {
|
||||
threadDeleted = DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
|
||||
}
|
||||
|
||||
if (threadDeleted) {
|
||||
threadId = -1;
|
||||
conversationViewModel.clearThreadId();
|
||||
messageCountsViewModel.clearThreadId();
|
||||
listener.setThreadId(threadId);
|
||||
}
|
||||
}
|
||||
@@ -678,13 +730,15 @@ public class ConversationFragment extends LoggingFragment {
|
||||
boolean threadDeleted;
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
threadDeleted = DatabaseFactory.getMmsDatabase(context).delete(messageRecord.getId());
|
||||
threadDeleted = DatabaseFactory.getMmsDatabase(context).deleteMessage(messageRecord.getId());
|
||||
} else {
|
||||
threadDeleted = DatabaseFactory.getSmsDatabase(context).deleteMessage(messageRecord.getId());
|
||||
}
|
||||
|
||||
if (threadDeleted) {
|
||||
threadId = -1;
|
||||
conversationViewModel.clearThreadId();
|
||||
messageCountsViewModel.clearThreadId();
|
||||
listener.setThreadId(threadId);
|
||||
}
|
||||
}
|
||||
@@ -725,12 +779,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisplayDetails(MessageRecord message) {
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message, recipient.getId(), threadId));
|
||||
private void handleDisplayDetails(ConversationMessage message) {
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message.getMessageRecord(), recipient.getId(), threadId));
|
||||
}
|
||||
|
||||
private void handleForwardMessage(MessageRecord message) {
|
||||
if (message.isViewOnce()) {
|
||||
private void handleForwardMessage(ConversationMessage conversationMessage) {
|
||||
if (conversationMessage.getMessageRecord().isViewOnce()) {
|
||||
throw new AssertionError("Cannot forward a view-once message.");
|
||||
}
|
||||
|
||||
@@ -738,10 +792,10 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Intent composeIntent = new Intent(getActivity(), ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody(requireContext()).toString());
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, conversationMessage.getDisplayBody(requireContext()));
|
||||
|
||||
if (message.isMms()) {
|
||||
MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
|
||||
if (conversationMessage.getMessageRecord().isMms()) {
|
||||
MmsMessageRecord mediaMessage = (MmsMessageRecord) conversationMessage.getMessageRecord();
|
||||
boolean isAlbum = mediaMessage.containsMediaSlide() &&
|
||||
mediaMessage.getSlideDeck().getSlides().size() > 1 &&
|
||||
mediaMessage.getSlideDeck().getAudioSlide() == null &&
|
||||
@@ -771,7 +825,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
Optional.fromNullable(attachment.getCaption()),
|
||||
Optional.absent()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!mediaList.isEmpty()) {
|
||||
composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
|
||||
@@ -784,6 +838,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
if (slide.hasSticker()) {
|
||||
composeIntent.putExtra(ConversationActivity.STICKER_EXTRA, slide.asAttachment().getSticker());
|
||||
composeIntent.setType(slide.asAttachment().getContentType());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,7 +867,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);
|
||||
}
|
||||
|
||||
private void handleReplyMessage(final MessageRecord message) {
|
||||
private void handleReplyMessage(final ConversationMessage message) {
|
||||
if (getActivity() != null) {
|
||||
//noinspection ConstantConditions
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
|
||||
@@ -853,12 +908,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
public long stageOutgoingMessage(OutgoingMediaMessage message) {
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(getContext()).readerFor(message, threadId).getCurrent();
|
||||
MessageRecord messageRecord = MmsDatabase.readerFor(message, threadId).getCurrent();
|
||||
|
||||
if (getListAdapter() != null) {
|
||||
clearHeaderIfNotTyping(getListAdapter());
|
||||
setLastSeen(0);
|
||||
getListAdapter().addFastRecord(messageRecord);
|
||||
getListAdapter().addFastRecord(ConversationMessageFactory.createWithResolvedData(messageRecord, messageRecord.getDisplayBody(requireContext()), message.getMentions()));
|
||||
list.post(() -> list.scrollToPosition(0));
|
||||
}
|
||||
|
||||
@@ -866,12 +921,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
public long stageOutgoingMessage(OutgoingTextMessage message) {
|
||||
MessageRecord messageRecord = DatabaseFactory.getSmsDatabase(getContext()).readerFor(message, threadId).getCurrent();
|
||||
MessageRecord messageRecord = SmsDatabase.readerFor(message, threadId).getCurrent();
|
||||
|
||||
if (getListAdapter() != null) {
|
||||
clearHeaderIfNotTyping(getListAdapter());
|
||||
setLastSeen(0);
|
||||
getListAdapter().addFastRecord(messageRecord);
|
||||
getListAdapter().addFastRecord(ConversationMessageFactory.createWithResolvedData(messageRecord));
|
||||
list.post(() -> list.scrollToPosition(0));
|
||||
}
|
||||
|
||||
@@ -910,6 +965,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
listener.onCursorChanged();
|
||||
|
||||
conversationScrollListener.onScrolled(list, 0, 0);
|
||||
};
|
||||
|
||||
int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition());
|
||||
@@ -921,7 +978,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
snapToTopDataObserver.buildScrollPosition(conversation.getJumpToPosition())
|
||||
.withOnScrollRequestComplete(() -> {
|
||||
afterScroll.run();
|
||||
getListAdapter().pulseHighlightItem(conversation.getJumpToPosition());
|
||||
getListAdapter().pulseAtPosition(conversation.getJumpToPosition());
|
||||
})
|
||||
.submit();
|
||||
} else if (conversation.isMessageRequestAccepted()) {
|
||||
@@ -959,27 +1016,40 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
public void jumpToMessage(@NonNull RecipientId author, long timestamp, @Nullable Runnable onMessageNotFound) {
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
return DatabaseFactory.getMmsSmsDatabase(getContext())
|
||||
.getMessagePositionInConversation(threadId, timestamp, author);
|
||||
}, p -> moveToMessagePosition(p + (isTypingIndicatorShowing() ? 1 : 0), onMessageNotFound));
|
||||
}, p -> moveToPosition(p + (isTypingIndicatorShowing() ? 1 : 0), onMessageNotFound));
|
||||
}
|
||||
|
||||
private void moveToMessagePosition(int position, @Nullable Runnable onMessageNotFound) {
|
||||
private void moveToPosition(int position, @Nullable Runnable onMessageNotFound) {
|
||||
conversationViewModel.onConversationDataAvailable(threadId, position);
|
||||
snapToTopDataObserver.buildScrollPosition(position)
|
||||
.withOnPerformScroll(((layoutManager, p) ->
|
||||
list.post(() -> {
|
||||
layoutManager.scrollToPosition(p);
|
||||
getListAdapter().pulseHighlightItem(position);
|
||||
})
|
||||
list.post(() -> {
|
||||
if (Math.abs(layoutManager.findFirstVisibleItemPosition() - p) < SCROLL_ANIMATION_THRESHOLD) {
|
||||
View child = layoutManager.findViewByPosition(position);
|
||||
|
||||
if (child != null && layoutManager.isViewPartiallyVisible(child, true, false)) {
|
||||
getListAdapter().pulseAtPosition(position);
|
||||
} else {
|
||||
pulsePosition = position;
|
||||
}
|
||||
|
||||
list.smoothScrollToPosition(p);
|
||||
} else {
|
||||
layoutManager.scrollToPosition(p);
|
||||
getListAdapter().pulseAtPosition(position);
|
||||
}
|
||||
})
|
||||
))
|
||||
.withOnInvalidPosition(() -> {
|
||||
if (onMessageNotFound != null) {
|
||||
onMessageNotFound.run();
|
||||
}
|
||||
Log.w(TAG, "[moveToMessagePosition] Tried to navigate to message, but it wasn't found.");
|
||||
Log.w(TAG, "[moveToMentionPosition] Tried to navigate to mention, but it wasn't found.");
|
||||
})
|
||||
.submit();
|
||||
}
|
||||
@@ -998,9 +1068,55 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeScrollButtonAnimations() {
|
||||
scrollButtonInAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_scale_in);
|
||||
scrollButtonOutAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_scale_out);
|
||||
|
||||
mentionButtonInAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_scale_in);
|
||||
mentionButtonOutAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_scale_out);
|
||||
|
||||
scrollButtonInAnimation.setDuration(100);
|
||||
scrollButtonOutAnimation.setDuration(50);
|
||||
|
||||
mentionButtonInAnimation.setDuration(100);
|
||||
mentionButtonOutAnimation.setDuration(50);
|
||||
}
|
||||
|
||||
private void scrollToNextMention() {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(ApplicationDependencies.getApplication());
|
||||
return mmsDatabase.getOldestUnreadMentionDetails(threadId);
|
||||
}, (pair) -> {
|
||||
if (pair != null) {
|
||||
jumpToMessage(pair.first(), pair.second(), () -> {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void postMarkAsReadRequest() {
|
||||
if (getListAdapter().hasNoConversationMessages()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int position = getListLayoutManager().findFirstVisibleItemPosition();
|
||||
if (position == getListAdapter().getItemCount() - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (position >= (isTypingIndicatorShowing() ? 1 : 0)) {
|
||||
ConversationMessage item = getListAdapter().getItem(position);
|
||||
if (item != null) {
|
||||
long timestamp = item.getMessageRecord()
|
||||
.getDateReceived();
|
||||
|
||||
markReadHelper.onViewsRevealed(timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ConversationFragmentListener {
|
||||
void setThreadId(long threadId);
|
||||
void handleReplyMessage(MessageRecord messageRecord);
|
||||
void handleReplyMessage(ConversationMessage conversationMessage);
|
||||
void onMessageActionToolbarOpened();
|
||||
void onForwardClicked();
|
||||
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
|
||||
@@ -1011,51 +1127,47 @@ public class ConversationFragment extends LoggingFragment {
|
||||
void onCursorChanged();
|
||||
void onListVerticalTranslationChanged(float translationY);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void handleReactionDetails(@NonNull View maskTarget);
|
||||
}
|
||||
|
||||
private class ConversationScrollListener extends OnScrollListener {
|
||||
|
||||
private final Animation scrollButtonInAnimation;
|
||||
private final Animation scrollButtonOutAnimation;
|
||||
private final ConversationDateHeader conversationDateHeader;
|
||||
|
||||
private boolean wasAtBottom = true;
|
||||
private boolean wasAtZoomScrollHeight = false;
|
||||
private long lastPositionId = -1;
|
||||
|
||||
ConversationScrollListener(@NonNull Context context) {
|
||||
this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in);
|
||||
this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out);
|
||||
this.conversationDateHeader = new ConversationDateHeader(context, scrollDateHeader);
|
||||
|
||||
this.scrollButtonInAnimation.setDuration(100);
|
||||
this.scrollButtonOutAnimation.setDuration(50);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull final RecyclerView rv, final int dx, final int dy) {
|
||||
boolean currentlyAtBottom = isAtBottom();
|
||||
boolean currentlyAtBottom = !rv.canScrollVertically(1);
|
||||
boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight();
|
||||
int positionId = getHeaderPositionId();
|
||||
|
||||
if (currentlyAtBottom && !wasAtBottom) {
|
||||
ViewUtil.fadeOut(composeDivider, 50, View.INVISIBLE);
|
||||
ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE);
|
||||
} else if (!currentlyAtBottom && wasAtBottom) {
|
||||
ViewUtil.fadeIn(composeDivider, 500);
|
||||
}
|
||||
|
||||
if (currentlyAtZoomScrollHeight && !wasAtZoomScrollHeight) {
|
||||
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
|
||||
if (currentlyAtBottom) {
|
||||
conversationViewModel.setShowScrollButtons(false);
|
||||
} else if (currentlyAtZoomScrollHeight) {
|
||||
conversationViewModel.setShowScrollButtons(true);
|
||||
}
|
||||
|
||||
if (positionId != lastPositionId) {
|
||||
bindScrollHeader(conversationDateHeader, positionId);
|
||||
}
|
||||
|
||||
wasAtBottom = currentlyAtBottom;
|
||||
wasAtZoomScrollHeight = currentlyAtZoomScrollHeight;
|
||||
lastPositionId = positionId;
|
||||
wasAtBottom = currentlyAtBottom;
|
||||
lastPositionId = positionId;
|
||||
|
||||
postMarkAsReadRequest();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1064,6 +1176,11 @@ public class ConversationFragment extends LoggingFragment {
|
||||
conversationDateHeader.show();
|
||||
} else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
conversationDateHeader.hide();
|
||||
|
||||
if (pulsePosition != -1) {
|
||||
getListAdapter().pulseAtPosition(pulsePosition);
|
||||
pulsePosition = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1085,9 +1202,9 @@ public class ConversationFragment extends LoggingFragment {
|
||||
private class ConversationFragmentItemClickListener implements ItemClickListener {
|
||||
|
||||
@Override
|
||||
public void onItemClick(MessageRecord messageRecord) {
|
||||
public void onItemClick(ConversationMessage conversationMessage) {
|
||||
if (actionMode != null) {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
if (getListAdapter().getSelectedItems().size() == 0) {
|
||||
@@ -1100,10 +1217,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemLongClick(View maskTarget, MessageRecord messageRecord) {
|
||||
public void onItemLongClick(View maskTarget, ConversationMessage conversationMessage) {
|
||||
|
||||
if (actionMode != null) return;
|
||||
|
||||
MessageRecord messageRecord = conversationMessage.getMessageRecord();
|
||||
|
||||
if (messageRecord.isSecure() &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
!messageRecord.isUpdate() &&
|
||||
@@ -1113,12 +1232,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
{
|
||||
isReacting = true;
|
||||
list.setLayoutFrozen(true);
|
||||
listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(messageRecord), () -> {
|
||||
listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(conversationMessage), () -> {
|
||||
isReacting = false;
|
||||
list.setLayoutFrozen(false);
|
||||
});
|
||||
} else {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
||||
@@ -1143,7 +1262,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
.getQuotedMessagePosition(threadId,
|
||||
messageRecord.getQuote().getId(),
|
||||
messageRecord.getQuote().getAuthor());
|
||||
}, p -> moveToMessagePosition(p + (isTypingIndicatorShowing() ? 1 : 0), () -> {
|
||||
}, p -> moveToPosition(p + (isTypingIndicatorShowing() ? 1 : 0), () -> {
|
||||
Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_no_longer_available, Toast.LENGTH_SHORT).show();
|
||||
}));
|
||||
}
|
||||
@@ -1198,7 +1317,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
.getViewOnceMessageManager()
|
||||
.scheduleIfNecessary();
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceViewOnceOpenJob(new MessagingDatabase.SyncMessageId(messageRecord.getIndividualRecipient().getId(), messageRecord.getDateSent())));
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceViewOnceOpenJob(new MessageDatabase.SyncMessageId(messageRecord.getIndividualRecipient().getId(), messageRecord.getDateSent())));
|
||||
|
||||
return tempUri;
|
||||
} catch (IOException e) {
|
||||
@@ -1259,14 +1378,15 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactionClicked(long messageId, boolean isMms) {
|
||||
public void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms) {
|
||||
if (getContext() == null) return;
|
||||
|
||||
listener.handleReactionDetails(reactionTarget);
|
||||
ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(requireFragmentManager(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId) {
|
||||
public void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId) {
|
||||
if (getContext() == null) return;
|
||||
|
||||
RecipientBottomSheetDialogFragment.create(recipientId, groupId).show(requireFragmentManager(), "BOTTOM");
|
||||
@@ -1276,6 +1396,11 @@ public class ConversationFragment extends LoggingFragment {
|
||||
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
listener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUrlClicked(@NonNull String url) {
|
||||
return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1287,8 +1412,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEnterMultiSelect(@NonNull MessageRecord messageRecord) {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
|
||||
private void handleEnterMultiSelect(@NonNull ConversationMessage conversationMessage) {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
||||
@@ -1342,23 +1467,23 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
private final ConversationMessage conversationMessage;
|
||||
|
||||
private ReactionsToolbarListener(@NonNull MessageRecord messageRecord) {
|
||||
this.messageRecord = messageRecord;
|
||||
private ReactionsToolbarListener(@NonNull ConversationMessage conversationMessage) {
|
||||
this.conversationMessage = conversationMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_info: handleDisplayDetails(messageRecord); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(messageRecord)); return true;
|
||||
case R.id.action_copy: handleCopyMessage(Sets.newHashSet(messageRecord)); return true;
|
||||
case R.id.action_reply: handleReplyMessage(messageRecord); return true;
|
||||
case R.id.action_multiselect: handleEnterMultiSelect(messageRecord); return true;
|
||||
case R.id.action_forward: handleForwardMessage(messageRecord); return true;
|
||||
case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) messageRecord); return true;
|
||||
default: return false;
|
||||
case R.id.action_info: handleDisplayDetails(conversationMessage); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(conversationMessage)); return true;
|
||||
case R.id.action_copy: handleCopyMessage(Sets.newHashSet(conversationMessage)); return true;
|
||||
case R.id.action_reply: handleReplyMessage(conversationMessage); return true;
|
||||
case R.id.action_multiselect: handleEnterMultiSelect(conversationMessage); return true;
|
||||
case R.id.action_forward: handleForwardMessage(conversationMessage); return true;
|
||||
case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord()); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1417,24 +1542,24 @@ public class ConversationFragment extends LoggingFragment {
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_details:
|
||||
handleDisplayDetails(getSelectedMessageRecord());
|
||||
handleDisplayDetails(getSelectedConversationMessage());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_forward:
|
||||
handleForwardMessage(getSelectedMessageRecord());
|
||||
handleForwardMessage(getSelectedConversationMessage());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_resend:
|
||||
handleResendMessage(getSelectedMessageRecord());
|
||||
handleResendMessage(getSelectedConversationMessage().getMessageRecord());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_save_attachment:
|
||||
handleSaveAttachment((MediaMmsMessageRecord)getSelectedMessageRecord());
|
||||
handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_reply:
|
||||
maybeShowSwipeToReplyTooltip();
|
||||
handleReplyMessage(getSelectedMessageRecord());
|
||||
handleReplyMessage(getSelectedConversationMessage());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -14,18 +14,30 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
class ConversationGroupViewModel extends ViewModel {
|
||||
import java.io.IOException;
|
||||
|
||||
private final MutableLiveData<Recipient> liveRecipient;
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
final class ConversationGroupViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Recipient> liveRecipient;
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
|
||||
|
||||
private ConversationGroupViewModel() {
|
||||
liveRecipient = new MutableLiveData<>();
|
||||
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, this::getGroupRecordForRecipient);
|
||||
groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, this::mapToGroupActiveState));
|
||||
this.liveRecipient = new MutableLiveData<>();
|
||||
|
||||
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
|
||||
|
||||
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
|
||||
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
|
||||
}
|
||||
|
||||
void onRecipientChange(Recipient recipient) {
|
||||
@@ -36,7 +48,11 @@ class ConversationGroupViewModel extends ViewModel {
|
||||
return groupActiveState;
|
||||
}
|
||||
|
||||
private GroupRecord getGroupRecordForRecipient(Recipient recipient) {
|
||||
LiveData<GroupDatabase.MemberLevel> getSelfMemberLevel() {
|
||||
return selfMembershipLevel;
|
||||
}
|
||||
|
||||
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient != null && recipient.isGroup()) {
|
||||
Application context = ApplicationDependencies.getApplication();
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
@@ -46,13 +62,37 @@ class ConversationGroupViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
|
||||
private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return new GroupActiveState(record.isActive(), record.isV2Group());
|
||||
}
|
||||
|
||||
private static GroupDatabase.MemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return record.memberLevel(Recipient.self());
|
||||
}
|
||||
|
||||
public static void onCancelJoinRequest(@NonNull Recipient recipient,
|
||||
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
|
||||
{
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
if (!recipient.isPushV2Group()) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
try {
|
||||
GroupManager.cancelJoinRequest(ApplicationDependencies.getApplication(), recipient.getGroupId().get().requireV2());
|
||||
callback.onComplete(null);
|
||||
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||
callback.onError(GroupChangeFailureReason.fromException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static final class GroupActiveState {
|
||||
private final boolean isActive;
|
||||
private final boolean isActiveV2;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
@@ -26,6 +27,7 @@ import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.text.Annotation;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -51,6 +53,7 @@ import androidx.annotation.DimenRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
@@ -62,6 +65,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.AlertView;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
@@ -69,11 +73,12 @@ import org.thoughtcrime.securesms.components.LinkPreviewView;
|
||||
import org.thoughtcrime.securesms.components.Outliner;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.components.SharedContactView;
|
||||
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
@@ -106,21 +111,27 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
|
||||
|
||||
/**
|
||||
* A view that displays an individual conversation item within a conversation
|
||||
* thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter.
|
||||
@@ -139,11 +150,13 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
private static final Rect SWIPE_RECT = new Rect();
|
||||
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
private ValueAnimator pulseOutlinerAlphaAnimator;
|
||||
|
||||
protected ConversationItemBodyBubble bodyBubble;
|
||||
protected View reply;
|
||||
@@ -160,8 +173,10 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private ViewGroup container;
|
||||
protected ReactionsConversationView reactionsView;
|
||||
|
||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||
private @NonNull Outliner outliner = new Outliner();
|
||||
private @NonNull Set<ConversationMessage> batchSelected = new HashSet<>();
|
||||
private @NonNull Outliner outliner = new Outliner();
|
||||
private @NonNull Outliner pulseOutliner = new Outliner();
|
||||
private @NonNull List<Outliner> outliners = new ArrayList<>(2);
|
||||
private LiveRecipient conversationRecipient;
|
||||
private Stub<ConversationItemThumbnail> mediaThumbnailStub;
|
||||
private Stub<AudioView> audioViewStub;
|
||||
@@ -182,6 +197,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
|
||||
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
|
||||
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
|
||||
private final UrlClickListener urlClickListener = new UrlClickListener();
|
||||
|
||||
private final Context context;
|
||||
|
||||
@@ -234,22 +250,23 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull MessageRecord messageRecord,
|
||||
public void bind(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseHighlight)
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<ConversationMessage> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulse)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
|
||||
|
||||
conversationRecipient = conversationRecipient.resolve();
|
||||
|
||||
this.messageRecord = messageRecord;
|
||||
this.conversationMessage = conversationMessage;
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.locale = locale;
|
||||
this.glideRequests = glideRequests;
|
||||
this.batchSelected = batchSelected;
|
||||
@@ -263,9 +280,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
setGutterSizes(messageRecord, groupThread);
|
||||
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread);
|
||||
setInteractionState(messageRecord, pulseHighlight);
|
||||
setBodyText(messageRecord, searchQuery);
|
||||
setBubbleState(messageRecord);
|
||||
setInteractionState(conversationMessage, pulse);
|
||||
setStatusIcons(messageRecord);
|
||||
setContactPhoto(recipient.get());
|
||||
setGroupMessageStatus(messageRecord, recipient.get());
|
||||
@@ -379,10 +396,11 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (conversationRecipient != null) {
|
||||
conversationRecipient.removeForeverObserver(this);
|
||||
}
|
||||
cancelPulseOutlinerAnimation();
|
||||
}
|
||||
|
||||
public MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
public ConversationMessage getConversationMessage() {
|
||||
return conversationMessage;
|
||||
}
|
||||
|
||||
/// MessageRecord Attribute Parsers
|
||||
@@ -403,7 +421,21 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_sent_text_secondary_color));
|
||||
bodyBubble.setOutliner(shouldDrawBodyBubbleOutline(messageRecord) ? outliner : null);
|
||||
|
||||
pulseOutliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_mention_pulse_color));
|
||||
pulseOutliner.setStrokeWidth(ViewUtil.dpToPx(4));
|
||||
|
||||
outliners.clear();
|
||||
if (shouldDrawBodyBubbleOutline(messageRecord)) {
|
||||
outliners.add(outliner);
|
||||
}
|
||||
outliners.add(pulseOutliner);
|
||||
|
||||
bodyBubble.setOutliners(outliners);
|
||||
|
||||
if (mediaThumbnailStub.resolved()) {
|
||||
mediaThumbnailStub.get().setPulseOutliner(pulseOutliner);
|
||||
}
|
||||
|
||||
if (audioViewStub.resolved()) {
|
||||
setAudioViewTint(messageRecord);
|
||||
@@ -424,36 +456,58 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
}
|
||||
|
||||
private void setInteractionState(MessageRecord messageRecord, boolean pulseHighlight) {
|
||||
if (batchSelected.contains(messageRecord)) {
|
||||
private void setInteractionState(ConversationMessage conversationMessage, boolean pulseMention) {
|
||||
if (batchSelected.contains(conversationMessage)) {
|
||||
setBackgroundResource(R.drawable.conversation_item_background);
|
||||
setSelected(true);
|
||||
} else if (pulseHighlight) {
|
||||
setBackgroundResource(R.drawable.conversation_item_background_animated);
|
||||
setSelected(true);
|
||||
postDelayed(() -> setSelected(false), 500);
|
||||
} else if (pulseMention) {
|
||||
setBackground(null);
|
||||
setSelected(false);
|
||||
startPulseOutlinerAnimation();
|
||||
} else {
|
||||
setSelected(false);
|
||||
}
|
||||
|
||||
if (mediaThumbnailStub.resolved()) {
|
||||
mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
|
||||
mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
|
||||
mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
|
||||
mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
|
||||
mediaThumbnailStub.get().setLongClickable(batchSelected.isEmpty());
|
||||
}
|
||||
|
||||
if (audioViewStub.resolved()) {
|
||||
audioViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
|
||||
audioViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
|
||||
audioViewStub.get().setClickable(batchSelected.isEmpty());
|
||||
audioViewStub.get().setEnabled(batchSelected.isEmpty());
|
||||
}
|
||||
|
||||
if (documentViewStub.resolved()) {
|
||||
documentViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
|
||||
documentViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
|
||||
documentViewStub.get().setClickable(batchSelected.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
private void startPulseOutlinerAnimation() {
|
||||
pulseOutlinerAlphaAnimator = ValueAnimator.ofInt(0, 0x66, 0).setDuration(600);
|
||||
pulseOutlinerAlphaAnimator.addUpdateListener(animator -> {
|
||||
pulseOutliner.setAlpha((Integer) animator.getAnimatedValue());
|
||||
bodyBubble.invalidate();
|
||||
|
||||
if (mediaThumbnailStub.resolved()) {
|
||||
mediaThumbnailStub.get().invalidate();
|
||||
}
|
||||
});
|
||||
pulseOutlinerAlphaAnimator.start();
|
||||
}
|
||||
|
||||
private void cancelPulseOutlinerAnimation() {
|
||||
if (pulseOutlinerAlphaAnimator != null) {
|
||||
pulseOutlinerAlphaAnimator.cancel();
|
||||
pulseOutlinerAlphaAnimator = null;
|
||||
}
|
||||
|
||||
pulseOutliner.setAlpha(0);
|
||||
}
|
||||
|
||||
private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord) {
|
||||
boolean isIncomingViewedOnce = !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
|
||||
return isIncomingViewedOnce || messageRecord.isRemoteDelete();
|
||||
@@ -516,10 +570,17 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
|
||||
private boolean hasBigImageLinkPreview(MessageRecord messageRecord) {
|
||||
if (!hasLinkPreview(messageRecord)) return false;
|
||||
if (!hasLinkPreview(messageRecord)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||
int minWidth = getResources().getDimensionPixelSize(R.dimen.media_bubble_min_width);
|
||||
|
||||
if (linkPreview.getThumbnail().isPresent() && !Util.isEmpty(linkPreview.getDescription())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int minWidth = getResources().getDimensionPixelSize(R.dimen.media_bubble_min_width_solo);
|
||||
|
||||
return linkPreview.getThumbnail().isPresent() &&
|
||||
linkPreview.getThumbnail().get().getWidth() >= minWidth &&
|
||||
@@ -530,7 +591,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
return messageRecord.isMms() && ((MmsMessageRecord) messageRecord).isViewOnce();
|
||||
}
|
||||
|
||||
private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery) {
|
||||
private void setBodyText(@NonNull MessageRecord messageRecord,
|
||||
@Nullable String searchQuery)
|
||||
{
|
||||
bodyText.setClickable(false);
|
||||
bodyText.setFocusable(false);
|
||||
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
|
||||
@@ -550,7 +613,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
} else if (isCaptionlessMms(messageRecord)) {
|
||||
bodyText.setVisibility(View.GONE);
|
||||
} else {
|
||||
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty());
|
||||
Spannable styledText = linkifyMessageBody(conversationMessage.getDisplayBody(getContext()), batchSelected.isEmpty());
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
|
||||
|
||||
@@ -560,6 +623,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
bodyText.setOverflowText(null);
|
||||
}
|
||||
|
||||
if (messageRecord.isOutgoing()) {
|
||||
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20));
|
||||
} else {
|
||||
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_40));
|
||||
}
|
||||
|
||||
bodyText.setText(styledText);
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
@@ -620,6 +689,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
if (hasBigImageLinkPreview(messageRecord)) {
|
||||
mediaThumbnailStub.get().setVisibility(VISIBLE);
|
||||
mediaThumbnailStub.get().setMinimumThumbnailWidth(readDimen(R.dimen.media_bubble_min_width_with_content));
|
||||
mediaThumbnailStub.get().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(context, linkPreview.getThumbnail().get())), showControls, false);
|
||||
mediaThumbnailStub.get().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
|
||||
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
|
||||
@@ -717,10 +787,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
|
||||
List<Slide> thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
|
||||
mediaThumbnailStub.get().setMinimumThumbnailWidth(readDimen(isCaptionlessMms(messageRecord) ? R.dimen.media_bubble_min_width_solo
|
||||
: R.dimen.media_bubble_min_width_with_content));
|
||||
mediaThumbnailStub.get().setImageResource(glideRequests,
|
||||
thumbnailSlides,
|
||||
showControls,
|
||||
@@ -853,14 +925,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
contactPhoto.setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onGroupMemberAvatarClicked(recipientId, conversationRecipient.get().requireGroupId());
|
||||
eventListener.onGroupMemberClicked(recipientId, conversationRecipient.get().requireGroupId());
|
||||
}
|
||||
});
|
||||
|
||||
contactPhoto.setAvatar(glideRequests, recipient, false);
|
||||
}
|
||||
|
||||
private SpannableString linkifyMessageBody(SpannableString messageBody, boolean shouldLinkifyAllLinks) {
|
||||
private SpannableString linkifyMessageBody(@NonNull SpannableString messageBody,
|
||||
boolean shouldLinkifyAllLinks)
|
||||
{
|
||||
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
|
||||
boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0);
|
||||
|
||||
@@ -872,11 +946,18 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
URLSpan[] urlSpans = messageBody.getSpans(0, messageBody.length(), URLSpan.class);
|
||||
|
||||
for (URLSpan urlSpan : urlSpans) {
|
||||
int start = messageBody.getSpanStart(urlSpan);
|
||||
int end = messageBody.getSpanEnd(urlSpan);
|
||||
messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
int start = messageBody.getSpanStart(urlSpan);
|
||||
int end = messageBody.getSpanEnd(urlSpan);
|
||||
URLSpan span = new InterceptableLongClickCopyLinkSpan(urlSpan.getURL(), urlClickListener);
|
||||
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(messageBody);
|
||||
for (Annotation annotation : mentionAnnotations) {
|
||||
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
return messageBody;
|
||||
}
|
||||
|
||||
@@ -899,7 +980,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
|
||||
//noinspection ConstantConditions
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment());
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getDisplayText(), quote.isOriginalMissing(), quote.getAttachment());
|
||||
quoteView.setVisibility(View.VISIBLE);
|
||||
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
@@ -968,7 +1049,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
reactionsView.setOnClickListener(v -> {
|
||||
if (eventListener == null) return;
|
||||
|
||||
eventListener.onReactionClicked(current.getId(), current.isMms());
|
||||
eventListener.onReactionClicked(this, current.getId(), current.isMms());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1077,33 +1158,41 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_alone;
|
||||
outliner.setRadius(bigRadius);
|
||||
pulseOutliner.setRadius(bigRadius);
|
||||
} else {
|
||||
background = R.drawable.message_bubble_background_received_alone;
|
||||
outliner.setRadius(bigRadius);
|
||||
pulseOutliner.setRadius(bigRadius);
|
||||
}
|
||||
} else if (isStartOfMessageCluster(current, previous, isGroupThread)) {
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_start;
|
||||
outliner.setRadii(bigRadius, bigRadius, smallRadius, bigRadius);
|
||||
pulseOutliner.setRadii(bigRadius, bigRadius, smallRadius, bigRadius);
|
||||
} else {
|
||||
background = R.drawable.message_bubble_background_received_start;
|
||||
outliner.setRadii(bigRadius, bigRadius, bigRadius, smallRadius);
|
||||
pulseOutliner.setRadii(bigRadius, bigRadius, bigRadius, smallRadius);
|
||||
}
|
||||
} else if (isEndOfMessageCluster(current, next, isGroupThread)) {
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_end;
|
||||
outliner.setRadii(bigRadius, smallRadius, bigRadius, bigRadius);
|
||||
pulseOutliner.setRadii(bigRadius, smallRadius, bigRadius, bigRadius);
|
||||
} else {
|
||||
background = R.drawable.message_bubble_background_received_end;
|
||||
outliner.setRadii(smallRadius, bigRadius, bigRadius, bigRadius);
|
||||
pulseOutliner.setRadii(smallRadius, bigRadius, bigRadius, bigRadius);
|
||||
}
|
||||
} else {
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_middle;
|
||||
outliner.setRadii(bigRadius, smallRadius, smallRadius, bigRadius);
|
||||
pulseOutliner.setRadii(bigRadius, smallRadius, smallRadius, bigRadius);
|
||||
} else {
|
||||
background = R.drawable.message_bubble_background_received_middle;
|
||||
outliner.setRadii(smallRadius, bigRadius, bigRadius, smallRadius);
|
||||
pulseOutliner.setRadii(smallRadius, bigRadius, bigRadius, smallRadius);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1403,6 +1492,33 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
}
|
||||
|
||||
private final class UrlClickListener implements UrlClickHandler {
|
||||
|
||||
@Override
|
||||
public boolean handleOnClick(@NonNull String url) {
|
||||
return eventListener != null && eventListener.onUrlClicked(url);
|
||||
}
|
||||
}
|
||||
|
||||
private class MentionClickableSpan extends ClickableSpan {
|
||||
private final RecipientId mentionedRecipientId;
|
||||
|
||||
MentionClickableSpan(RecipientId mentionedRecipientId) {
|
||||
this.mentionedRecipientId = mentionedRecipientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (eventListener != null && !Recipient.resolved(mentionedRecipientId).isLocalNumber()) {
|
||||
VibrateUtil.vibrateTick(context);
|
||||
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) { }
|
||||
}
|
||||
|
||||
private void handleMessageApproval() {
|
||||
final int title;
|
||||
final int message;
|
||||
@@ -1418,21 +1534,18 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (message > -1) builder.setMessage(message);
|
||||
|
||||
builder.setPositiveButton(R.string.yes, (dialogInterface, i) -> {
|
||||
if (messageRecord.isMms()) {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
database.markAsInsecure(messageRecord.getId());
|
||||
database.markAsOutbox(messageRecord.getId());
|
||||
database.markAsForcedSms(messageRecord.getId());
|
||||
MessageDatabase db = messageRecord.isMms() ? DatabaseFactory.getMmsDatabase(context)
|
||||
: DatabaseFactory.getSmsDatabase(context);
|
||||
|
||||
db.markAsInsecure(messageRecord.getId());
|
||||
db.markAsOutbox(messageRecord.getId());
|
||||
db.markAsForcedSms(messageRecord.getId());
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
MmsSendJob.enqueue(context,
|
||||
ApplicationDependencies.getJobManager(),
|
||||
messageRecord.getId());
|
||||
} else {
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
database.markAsInsecure(messageRecord.getId());
|
||||
database.markAsOutbox(messageRecord.getId());
|
||||
database.markAsForcedSms(messageRecord.getId());
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new SmsSendJob(messageRecord.getId(),
|
||||
messageRecord.getIndividualRecipient()));
|
||||
}
|
||||
|
||||
@@ -5,13 +5,18 @@ import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.Outliner;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ConversationItemBodyBubble extends LinearLayout {
|
||||
|
||||
@Nullable private Outliner outliner;
|
||||
@Nullable private List<Outliner> outliners = Collections.emptyList();
|
||||
@Nullable private OnSizeChangedListener sizeChangedListener;
|
||||
|
||||
public ConversationItemBodyBubble(Context context) {
|
||||
@@ -26,8 +31,8 @@ public class ConversationItemBodyBubble extends LinearLayout {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setOutliner(@Nullable Outliner outliner) {
|
||||
this.outliner = outliner;
|
||||
public void setOutliners(@NonNull List<Outliner> outliners) {
|
||||
this.outliners = outliners;
|
||||
}
|
||||
|
||||
public void setOnSizeChangedListener(@Nullable OnSizeChangedListener listener) {
|
||||
@@ -38,9 +43,11 @@ public class ConversationItemBodyBubble extends LinearLayout {
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (outliner == null) return;
|
||||
if (Util.isEmpty(outliners)) return;
|
||||
|
||||
outliner.draw(canvas, 0, getMeasuredWidth(), getMeasuredHeight(), 0);
|
||||
for (Outliner outliner : outliners) {
|
||||
outliner.draw(canvas, 0, getMeasuredWidth(), getMeasuredHeight(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -114,8 +114,8 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
private void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (cannotSwipeViewHolder(viewHolder)) return;
|
||||
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
MessageRecord messageRecord = item.getMessageRecord();
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
ConversationMessage messageRecord = item.getConversationMessage();
|
||||
|
||||
onSwipeListener.onSwipe(messageRecord);
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
if (!(viewHolder.itemView instanceof ConversationItem)) return true;
|
||||
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord()) ||
|
||||
return !swipeAvailabilityProvider.isSwipeAvailable(item.getConversationMessage()) ||
|
||||
item.disallowSwipe(latestDownX, latestDownY);
|
||||
}
|
||||
|
||||
@@ -192,10 +192,10 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
}
|
||||
|
||||
interface SwipeAvailabilityProvider {
|
||||
boolean isSwipeAvailable(MessageRecord messageRecord);
|
||||
boolean isSwipeAvailable(ConversationMessage conversationMessage);
|
||||
}
|
||||
|
||||
interface OnSwipeListener {
|
||||
void onSwipe(MessageRecord messageRecord);
|
||||
void onSwipe(ConversationMessage conversationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A view level model used to pass arbitrary message related information needed
|
||||
* for various presentations.
|
||||
*/
|
||||
public class ConversationMessage {
|
||||
@NonNull private final MessageRecord messageRecord;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
@Nullable private final SpannableString body;
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord) {
|
||||
this(messageRecord, null, null);
|
||||
}
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord,
|
||||
@Nullable CharSequence body,
|
||||
@Nullable List<Mention> mentions)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.body = body != null ? SpannableString.valueOf(body) : null;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
|
||||
if (!this.mentions.isEmpty() && this.body != null) {
|
||||
MentionAnnotation.setMentionAnnotations(this.body, this.mentions);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final ConversationMessage that = (ConversationMessage) o;
|
||||
return messageRecord.equals(that.messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return messageRecord.hashCode();
|
||||
}
|
||||
|
||||
public long getUniqueId(@NonNull MessageDigest digest) {
|
||||
String unique = (messageRecord.isMms() ? "MMS::" : "SMS::") + messageRecord.getId();
|
||||
byte[] bytes = digest.digest(unique.getBytes());
|
||||
|
||||
return Conversions.byteArrayToLong(bytes);
|
||||
}
|
||||
|
||||
public @NonNull SpannableString getDisplayBody(Context context) {
|
||||
if (mentions.isEmpty() || body == null) {
|
||||
return messageRecord.getDisplayBody(context);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory providing multiple ways of creating {@link ConversationMessage}s.
|
||||
*/
|
||||
public static class ConversationMessageFactory {
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord. No database or
|
||||
* heavy work performed as the message is assumed to not have any mentions.
|
||||
*/
|
||||
@AnyThread
|
||||
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord) {
|
||||
return new ConversationMessage(messageRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, potentially annotated body, and
|
||||
* list of actual mentions. No database or heavy work performed as the body and mentions are assumed to be
|
||||
* fully updated with display names.
|
||||
*
|
||||
* @param body Contains appropriate {@link MentionAnnotation}s and is updated with actual profile names.
|
||||
* @param mentions List of actual mentions (i.e., not placeholder) matching annotation ranges in body.
|
||||
*/
|
||||
@AnyThread
|
||||
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @Nullable List<Mention> mentions) {
|
||||
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
|
||||
return new ConversationMessage(messageRecord, body, mentions);
|
||||
}
|
||||
return createWithResolvedData(messageRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord and will update and modify the provided
|
||||
* mentions from placeholder to actual. This method may perform database operations to resolve mentions to display names.
|
||||
*
|
||||
* @param mentions List of placeholder mentions to be used to update the body in the provided MessageRecord.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @Nullable List<Mention> mentions) {
|
||||
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
|
||||
}
|
||||
return createWithResolvedData(messageRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, and will query for potential mentions. If mentions
|
||||
* are found, the body of the provided message will be updated and modified to match actual mentions. This will perform
|
||||
* database operations to query for mentions and then to resolve mentions to display names.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessage(messageRecord.getId());
|
||||
if (!mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
|
||||
}
|
||||
}
|
||||
return createWithResolvedData(messageRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import android.animation.AnimatorSet;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
@@ -19,7 +21,6 @@ import android.widget.RelativeLayout;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
@@ -32,10 +33,11 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.MaskView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -91,6 +93,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
private OnHideListener onHideListener;
|
||||
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet revealMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAllButMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideMaskAnimatorSet = new AnimatorSet();
|
||||
@@ -185,16 +188,31 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
maskView.setTarget(maskTarget);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
toolbar.setVisibility(VISIBLE);
|
||||
setVisibility(View.VISIBLE);
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.activity = activity;
|
||||
originalStatusBarColor = activity.getWindow().getStatusBarColor();
|
||||
activity.getWindow().setStatusBarColor(ContextCompat.getColor(activity, R.color.action_mode_status_bar));
|
||||
activity.getWindow().setStatusBarColor(ThemeUtil.getThemedColor(getContext(), R.attr.reactions_overlay_toolbar_background_color));
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext()) && Build.VERSION.SDK_INT >= 23) {
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
|
||||
maskView.setPadding(0, maskPaddingTop, 0, maskPaddingBottom);
|
||||
maskView.setTarget(maskTarget);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
toolbar.setVisibility(GONE);
|
||||
setVisibility(VISIBLE);
|
||||
revealMaskAnimatorSet.start();
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
maskView.setTarget(null);
|
||||
hideInternal(hideAnimatorSet, onHideListener);
|
||||
@@ -218,8 +236,9 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
revealAnimatorSet.end();
|
||||
hideAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
if (Build.VERSION.SDK_INT >= 23 && activity != null) {
|
||||
activity.getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
@@ -317,7 +336,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
selected = getSelectedIndexViaDownEvent(motionEvent);
|
||||
|
||||
if (selected == -1) {
|
||||
if (motionEvent.getRawY() < toolbar.getHeight() + statusBarHeight) {
|
||||
if (motionEvent.getY() < toolbar.getHeight() + statusBarHeight) {
|
||||
isToolbarTouch = true;
|
||||
return false;
|
||||
}
|
||||
@@ -358,7 +377,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
view.setTranslationY(0);
|
||||
|
||||
boolean isAtCustomIndex = i == customEmojiIndex;
|
||||
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && ReactionEmoji.values()[i].emoji.equals(oldEmoji);
|
||||
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && oldEmoji != null && ReactionEmoji.values()[i].emoji.equals(EmojiUtil.getCanonicalRepresentation(oldEmoji));
|
||||
boolean isAtCustomIndexAndOldEmojiExists = isAtCustomIndex && oldEmoji != null;
|
||||
|
||||
if (!foundSelected &&
|
||||
@@ -379,13 +398,13 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
view.setImageEmoji(oldEmoji);
|
||||
view.setTag(oldEmoji);
|
||||
} else {
|
||||
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
|
||||
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
|
||||
}
|
||||
} else if (isAtCustomIndex) {
|
||||
view.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.ic_any_emoji_32));
|
||||
view.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.reactions_overlay_custom_emoji_icon));
|
||||
view.setTag(null);
|
||||
} else {
|
||||
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
|
||||
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,7 +466,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
if (selected == customEmojiIndex) {
|
||||
onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
|
||||
} else {
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, ReactionEmoji.values()[selected].emoji);
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[selected].emoji));
|
||||
}
|
||||
} else {
|
||||
hide();
|
||||
@@ -534,6 +553,9 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
revealAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealAnimatorSet.playTogether(reveals);
|
||||
|
||||
revealMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealMaskAnimatorSet.playTogether(overlayRevealAnim);
|
||||
|
||||
List<Animator> hides = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
|
||||
@@ -159,7 +159,7 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
}
|
||||
|
||||
private void setIndividualRecipientTitle(Recipient recipient) {
|
||||
final String displayName = recipient.getDisplayName(getContext());
|
||||
final String displayName = recipient.getDisplayNameOrUsername(getContext());
|
||||
this.title.setText(displayName);
|
||||
this.subtitle.setText(null);
|
||||
updateVerifiedSubtitleVisibility();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user