mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-17 16:36:06 +00:00
Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0068d62122 | ||
|
|
3f1fa59e09 | ||
|
|
df5114c62c | ||
|
|
956e3924ff | ||
|
|
20ad166e0f | ||
|
|
12ea88f409 | ||
|
|
0c5648bfb1 | ||
|
|
91ca19f294 | ||
|
|
71250afd2c | ||
|
|
cfdef7bca7 | ||
|
|
872f935fd5 | ||
|
|
0ed1f73990 | ||
|
|
349a2f72cb | ||
|
|
2b4a4d6109 | ||
|
|
9f882d2fbb | ||
|
|
cb4a9730aa | ||
|
|
e0657d09d8 | ||
|
|
01b9cb13b4 | ||
|
|
2c7260557c | ||
|
|
9e5156ab73 | ||
|
|
3dc1614fbc | ||
|
|
2f69a9c38e | ||
|
|
5e536c3fa5 | ||
|
|
6bb9d27d4e | ||
|
|
2d1bf33902 | ||
|
|
985a220fca | ||
|
|
31e137cf6d | ||
|
|
f796447815 | ||
|
|
936e772ba0 | ||
|
|
ecee797d00 | ||
|
|
357a8fc124 | ||
|
|
1233af0ddd | ||
|
|
a264d10685 | ||
|
|
ed17701a0a | ||
|
|
49e1ccea28 | ||
|
|
4c43b0d1e3 | ||
|
|
5ce09defca | ||
|
|
da9064b714 | ||
|
|
7bb53e4b06 | ||
|
|
6a4ce1b658 | ||
|
|
f84595e1e8 | ||
|
|
41d5c54033 | ||
|
|
b9d6b63c09 | ||
|
|
506ad0b3f1 | ||
|
|
c8302174a9 | ||
|
|
39cebfbb4e | ||
|
|
d36ec9af47 | ||
|
|
5f6d971bf7 | ||
|
|
7a722d92a3 | ||
|
|
0bf0eba450 | ||
|
|
d40783f794 | ||
|
|
88733473e2 | ||
|
|
7b65533095 | ||
|
|
52b533c121 | ||
|
|
a4fa2e14fb | ||
|
|
6933f1d818 | ||
|
|
b5d6cb2a8d | ||
|
|
d1478c5ce0 | ||
|
|
fbe62f0f3e | ||
|
|
f84705b756 | ||
|
|
cf2189c11a | ||
|
|
dfc4178252 | ||
|
|
07952f2146 | ||
|
|
a90dad22a9 | ||
|
|
64f7330609 | ||
|
|
5e382c120b | ||
|
|
3eea568f5f | ||
|
|
0077b29d6e | ||
|
|
dfa6306b61 | ||
|
|
a4bf075a1a | ||
|
|
373d622535 | ||
|
|
ba1df58eb3 | ||
|
|
9fb85f7c76 | ||
|
|
5e58f0a212 | ||
|
|
8fa01f13e9 | ||
|
|
4ce136be17 | ||
|
|
4099154dc0 | ||
|
|
3f983a5c82 | ||
|
|
9743e3689a | ||
|
|
1363f55f77 | ||
|
|
f1d98f6c7b | ||
|
|
9279a54d28 | ||
|
|
81889d8130 | ||
|
|
6aecb8fbc1 | ||
|
|
8aa413032d | ||
|
|
5bc4686eb8 | ||
|
|
f676d1c61c | ||
|
|
ac54b5cbdf | ||
|
|
b4b1e5b605 | ||
|
|
5eace49739 | ||
|
|
e93d7518f3 | ||
|
|
9c97cd8816 | ||
|
|
90f20c36c5 | ||
|
|
9f8dd7992a | ||
|
|
f4d3fe9176 | ||
|
|
ffc7c13717 | ||
|
|
daf93c473b | ||
|
|
d21782696a | ||
|
|
3357475fc4 | ||
|
|
ead64d92a5 | ||
|
|
5eaac6cb17 | ||
|
|
b3f0a44f10 | ||
|
|
e4d0e2f730 | ||
|
|
492a42883e | ||
|
|
b182f73415 | ||
|
|
e766b9737e | ||
|
|
2335f93579 | ||
|
|
1730260343 | ||
|
|
27506e9ed8 | ||
|
|
dc64a186d5 | ||
|
|
3163e09b98 | ||
|
|
dcb9978bb1 | ||
|
|
4a94a0a5c5 | ||
|
|
8a2d20403e | ||
|
|
ec706e95cc | ||
|
|
bd3b14a27f | ||
|
|
082d9e852c | ||
|
|
36da519b26 | ||
|
|
06ffdde892 | ||
|
|
1ec57c080c | ||
|
|
a635f27c68 | ||
|
|
ee3d7a9a35 | ||
|
|
9a1c869efe | ||
|
|
837ed76f85 | ||
|
|
b46589cd14 | ||
|
|
d04e4606d2 | ||
|
|
385bd0eb8a | ||
|
|
089656e5c4 | ||
|
|
84ec6dd458 | ||
|
|
322c139c26 | ||
|
|
babe1833bb | ||
|
|
9effa47dd8 | ||
|
|
7ef57cc0cf | ||
|
|
97420aae1b | ||
|
|
415e6309f9 | ||
|
|
83e63ff854 | ||
|
|
de7f103130 | ||
|
|
2cb912681d | ||
|
|
04bdf94b78 | ||
|
|
c7389ddaa7 | ||
|
|
e778ab2e3a | ||
|
|
533d86607f | ||
|
|
cb2096670f | ||
|
|
284f221a9d | ||
|
|
bc639dd438 | ||
|
|
1baddbb40e | ||
|
|
f784dab868 | ||
|
|
85192aaa21 | ||
|
|
054c705fe2 | ||
|
|
07b0d8cf6e | ||
|
|
597d16f566 | ||
|
|
0ca2c781c3 | ||
|
|
f642de9c41 | ||
|
|
8965388d05 | ||
|
|
58c4582f15 | ||
|
|
44bc1b5cc0 | ||
|
|
714ebb3e08 | ||
|
|
8f871c2e3a | ||
|
|
5cdc5bc441 | ||
|
|
8d060837ad | ||
|
|
1d230d4cd6 | ||
|
|
3636ae7667 | ||
|
|
9ffb5112c6 | ||
|
|
ca5d574cd7 | ||
|
|
c80283dbcc | ||
|
|
3fcaddf2d3 | ||
|
|
6ecff5bce9 | ||
|
|
a103c7dcb6 | ||
|
|
63746bbb47 | ||
|
|
ed0be6fc9a | ||
|
|
26404ff5d7 | ||
|
|
adf1674877 | ||
|
|
ab2235fc88 | ||
|
|
441a6d3fe7 | ||
|
|
e00397620a | ||
|
|
38fa58c0a3 | ||
|
|
b40fd7b243 | ||
|
|
ae34877496 | ||
|
|
599cf1e5cb | ||
|
|
474963dcf1 | ||
|
|
e22384b6b4 | ||
|
|
fb00652396 | ||
|
|
a5dbb5d91f | ||
|
|
e75a03b6f8 | ||
|
|
eb7fe7f3e0 | ||
|
|
3179808f17 | ||
|
|
fde9f05bd0 | ||
|
|
8de4290c5b | ||
|
|
19c74c8872 | ||
|
|
50edb5d1f4 | ||
|
|
c6ccfd7e75 | ||
|
|
3796ce69e4 | ||
|
|
9835e31b46 | ||
|
|
a35040c909 | ||
|
|
a4c94638ca | ||
|
|
e70a8ae6a0 | ||
|
|
100359e38d | ||
|
|
cd995aca56 | ||
|
|
3a4bae88ca | ||
|
|
e60eae27fb | ||
|
|
cd6c01e230 | ||
|
|
0af264429f | ||
|
|
a6d3862350 | ||
|
|
3fca4850dd | ||
|
|
ba7e41d9a6 | ||
|
|
fe33ce3413 | ||
|
|
4e25e8aaa2 | ||
|
|
91be826c7d | ||
|
|
fdfe0cddb8 | ||
|
|
e8ef62116f | ||
|
|
caf8bb39d8 | ||
|
|
222ba6ee53 | ||
|
|
8dcda73072 |
1
.github/workflows/android.yml
vendored
1
.github/workflows/android.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches:
|
||||
- 'master'
|
||||
- '4.**'
|
||||
- '5.**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
18
.github/workflows/docker.yml
vendored
Normal file
18
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Reproducible Build Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: cd reproducible-builds && docker build -t signal-android . && cd ..
|
||||
|
||||
- name: Test build
|
||||
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assembleRelease
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
.classpath
|
||||
captures/
|
||||
project.properties
|
||||
keystore.debug.properties
|
||||
keystore.staging.properties
|
||||
.project
|
||||
.settings
|
||||
bin/
|
||||
@@ -23,6 +25,5 @@ ffpr
|
||||
test/androidTestEspresso/res/values/arrays.xml
|
||||
obj/
|
||||
jni/libspeex/.deps/
|
||||
*.sh
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -1,25 +0,0 @@
|
||||
FROM ubuntu:17.10
|
||||
|
||||
RUN dpkg --add-architecture i386 && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y software-properties-common && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y libc6:i386=2.26-0ubuntu2.1 libncurses5:i386=6.0+20160625-1ubuntu1 libstdc++6:i386=7.2.0-8ubuntu3.2 lib32z1=1:1.2.11.dfsg-0ubuntu2 wget openjdk-8-jdk=8u171-b11-0ubuntu0.17.10.1 git unzip opensc pcscd && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean
|
||||
|
||||
ENV ANDROID_SDK_FILENAME android-sdk_r24.4.1-linux.tgz
|
||||
ENV ANDROID_SDK_URL https://dl.google.com/android/${ANDROID_SDK_FILENAME}
|
||||
ENV ANDROID_API_LEVELS android-28
|
||||
ENV ANDROID_BUILD_TOOLS_VERSION 28.0.3
|
||||
ENV ANDROID_HOME /usr/local/android-sdk-linux
|
||||
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
|
||||
RUN cd /usr/local/ && \
|
||||
wget -q ${ANDROID_SDK_URL} && \
|
||||
tar -xzf ${ANDROID_SDK_FILENAME} && \
|
||||
rm ${ANDROID_SDK_FILENAME}
|
||||
RUN echo y | android update sdk --no-ui -a --filter ${ANDROID_API_LEVELS}
|
||||
RUN echo y | android update sdk --no-ui -a --filter extra-android-m2repository,extra-android-support,extra-google-google_play_services,extra-google-m2repository
|
||||
RUN echo y | android update sdk --no-ui -a --filter tools,platform-tools,build-tools-${ANDROID_BUILD_TOOLS_VERSION}
|
||||
RUN rm -rf ${ANDROID_HOME}/tools && unzip ${ANDROID_HOME}/temp/*.zip -d ${ANDROID_HOME}
|
||||
136
app/build.gradle
136
app/build.gradle
@@ -15,7 +15,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
|
||||
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
||||
}
|
||||
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 711
|
||||
def canonicalVersionName = "4.72.2"
|
||||
def canonicalVersionCode = 736
|
||||
def canonicalVersionName = "4.76.3"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -90,10 +90,12 @@ def abiPostFix = ['universal' : 0,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
android {
|
||||
flavorDimensions "none"
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
flavorDimensions 'distribution', 'environment'
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
dexOptions {
|
||||
@@ -101,11 +103,13 @@ android {
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
staging {
|
||||
storeFile file("${project.rootDir}/dev.keystore")
|
||||
storePassword 'android'
|
||||
keyAlias 'staging'
|
||||
keyPassword 'android'
|
||||
if (keystores.debug != null) {
|
||||
debug {
|
||||
storeFile file("${project.rootDir}/${keystores.debug.storeFile}")
|
||||
storePassword keystores.debug.storePassword
|
||||
keyAlias keystores.debug.keyAlias
|
||||
keyPassword keystores.debug.keyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +118,7 @@ android {
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 29
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -132,9 +136,10 @@ android {
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_SERVICE_ID", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
@@ -179,6 +184,10 @@ android {
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
if (keystores['debug'] != null) {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
isDefault true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||
'proguard/proguard-firebase-messaging.pro',
|
||||
@@ -202,25 +211,9 @@ android {
|
||||
testProguardFiles 'proguard/proguard-automation.pro',
|
||||
'proguard/proguard.cfg'
|
||||
}
|
||||
staging {
|
||||
initWith debug
|
||||
applicationIdSuffix ".staging"
|
||||
signingConfig signingConfigs.staging
|
||||
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
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", "\"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=\""
|
||||
}
|
||||
flipper {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
}
|
||||
release {
|
||||
@@ -231,18 +224,52 @@ android {
|
||||
|
||||
productFlavors {
|
||||
play {
|
||||
dimension "none"
|
||||
dimension 'distribution'
|
||||
isDefault true
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
website {
|
||||
dimension "none"
|
||||
dimension 'distribution'
|
||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
}
|
||||
|
||||
internal {
|
||||
dimension 'distribution'
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
prod {
|
||||
dimension 'environment'
|
||||
|
||||
isDefault true
|
||||
}
|
||||
|
||||
staging {
|
||||
dimension 'environment'
|
||||
|
||||
applicationIdSuffix ".staging"
|
||||
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
@@ -312,6 +339,7 @@ dependencies {
|
||||
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
|
||||
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
@@ -321,7 +349,7 @@ dependencies {
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.7.0'
|
||||
implementation 'org.signal:ringrtc-android:2.7.3'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
@@ -382,17 +410,18 @@ dependencies {
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.2') {
|
||||
testImplementation ('org.robolectric:robolectric:4.4') {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
||||
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
|
||||
}
|
||||
|
||||
|
||||
@@ -439,28 +468,24 @@ def signProductionRelease = { variant ->
|
||||
|
||||
task signProductionPlayRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionInternalRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionWebsiteRelease {
|
||||
doLast {
|
||||
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
|
||||
def variant = android.applicationVariants.find { (it.name == 'websiteProdRelease') }
|
||||
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
||||
assembleWebsiteDescriptor(variant, signedRelease)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.whenTaskAdded { task ->
|
||||
if (task.name.equals("assemblePlayRelease")) {
|
||||
task.finalizedBy signProductionPlayRelease
|
||||
}
|
||||
|
||||
if (task.name.equals("assembleWebsiteRelease")) {
|
||||
task.finalizedBy signProductionWebsiteRelease
|
||||
}
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
def result = exec {
|
||||
@@ -482,3 +507,14 @@ tasks.withType(Test) {
|
||||
showStackTraces true
|
||||
}
|
||||
}
|
||||
|
||||
def loadKeystoreProperties(filename) {
|
||||
def keystorePropertiesFile = file("${project.rootDir}/${filename}")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
return keystoreProperties;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<resources>
|
||||
<string name="app_name">Signal (Flipper)</string>
|
||||
</resources>
|
||||
5
app/src/internal/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/internal/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/core_red_shade"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -36,7 +36,8 @@
|
||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
@@ -223,6 +224,14 @@
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="signal.group" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true"
|
||||
tools:targetApi="23">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -533,6 +542,18 @@
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||
|
||||
<service android:name=".components.voice.VoiceNotePlaybackService">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name=".service.QuickResponseService"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
||||
android:exported="true" >
|
||||
@@ -652,6 +673,11 @@
|
||||
android:exported="false"
|
||||
android:authorities="${applicationId}.part" />
|
||||
|
||||
<provider android:name=".providers.BlobContentProvider"
|
||||
android:authorities="${applicationId}.blob"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<provider android:name=".providers.MmsBodyProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
|
||||
BIN
app/src/main/assets/sounds/state-change_confirm-down.ogg
Executable file
BIN
app/src/main/assets/sounds/state-change_confirm-down.ogg
Executable file
Binary file not shown.
BIN
app/src/main/assets/sounds/state-change_confirm-up.ogg
Executable file
BIN
app/src/main/assets/sounds/state-change_confirm-up.ogg
Executable file
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
|
||||
public final class AppCapabilities {
|
||||
@@ -15,6 +16,6 @@ public final class AppCapabilities {
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable);
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, FeatureFlags.groupsV1AutoMigration());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
@@ -155,6 +156,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
@@ -64,6 +65,8 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
|
||||
|
||||
@@ -96,6 +99,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
|
||||
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
|
||||
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
|
||||
initFragment(android.R.id.content, new BackupsPreferenceFragment());
|
||||
} else if (icicle == null) {
|
||||
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
|
||||
}
|
||||
|
||||
@@ -82,10 +82,10 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
||||
|
||||
Recipient.live(recipientId).observe(this, recipient -> {
|
||||
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||
: recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||
: recipient.getFallbackContactPhoto();
|
||||
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||
: recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||
: recipient.getFallbackContactPhoto();
|
||||
|
||||
Resources resources = this.getResources();
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -51,6 +54,12 @@ public interface BindableConversationItem extends Unbindable {
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onVoiceNotePause(@NonNull Uri uri);
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
|
||||
void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.Manifest;
|
||||
import android.animation.LayoutTransition;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
@@ -29,7 +30,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.CycleInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.TextView;
|
||||
@@ -56,6 +56,7 @@ import com.google.android.material.chip.ChipGroup;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.components.emoji.WarningTextView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactChip;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
@@ -63,6 +64,8 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -82,7 +85,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -103,11 +105,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||
|
||||
public static final String DISPLAY_MODE = "display_mode";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final String SELECTION_LIMIT = "selection_limit";
|
||||
public static final String SELECTION_LIMITS = "selection_limits";
|
||||
public static final String CURRENT_SELECTION = "current_selection";
|
||||
public static final String HIDE_COUNT = "hide_count";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
@@ -123,15 +125,17 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private TextView groupLimit;
|
||||
private WarningTextView groupLimit;
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@Nullable private ListCallback listCallback;
|
||||
@Nullable private ScrollCallback scrollCallback;
|
||||
private GlideRequests glideRequests;
|
||||
private int selectionLimit;
|
||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -206,9 +210,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
});
|
||||
|
||||
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||
Intent intent = requireActivity().getIntent();
|
||||
|
||||
swipeRefresh.setEnabled(intent.getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
hideCount = intent.getBooleanExtra(HIDE_COUNT, false);
|
||||
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
||||
isMulti = selectionLimit != null;
|
||||
|
||||
if (!isMulti) {
|
||||
selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
}
|
||||
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
@@ -217,12 +230,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
private void updateGroupLimit(int chipCount) {
|
||||
if (selectionLimit != NO_LIMIT) {
|
||||
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
|
||||
groupLimit.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
groupLimit.setVisibility(View.GONE);
|
||||
}
|
||||
int members = currentSelection.size() + chipCount;
|
||||
groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
|
||||
groupLimit.setVisibility(isMulti && !hideCount ? View.VISIBLE : View.GONE);
|
||||
groupLimit.setWarning(selectionWarningLimitExceeded());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -254,7 +265,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
public boolean isMulti() {
|
||||
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||
return isMulti;
|
||||
}
|
||||
|
||||
private void initializeCursor() {
|
||||
@@ -264,7 +275,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
glideRequests,
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti(),
|
||||
isMulti,
|
||||
currentSelection);
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
@@ -450,15 +461,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
|
||||
if (isMulti() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (selectionLimitReached()) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
|
||||
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
|
||||
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (selectionHardLimitReached()) {
|
||||
GroupLimitDialog.showHardLimitMessage(requireContext());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -486,7 +496,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
||||
.setPositiveButton(R.string.ContactSelectionListFragment_okay, (dialog, which) -> dialog.dismiss())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
});
|
||||
@@ -508,16 +518,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean selectionLimitReached() {
|
||||
return getChipCount() + currentSelection.size() >= selectionLimit;
|
||||
private boolean selectionHardLimitReached() {
|
||||
return getChipCount() + currentSelection.size() >= selectionLimit.getHardLimit();
|
||||
}
|
||||
|
||||
private boolean selectionWarningLimitReachedExactly() {
|
||||
return getChipCount() + currentSelection.size() == selectionLimit.getRecommendedLimit();
|
||||
}
|
||||
|
||||
private boolean selectionWarningLimitExceeded() {
|
||||
return getChipCount() + currentSelection.size() > selectionLimit.getRecommendedLimit();
|
||||
}
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
||||
if (isMulti()) {
|
||||
if (isMulti) {
|
||||
addChipForSelectedContact(selectedContact);
|
||||
}
|
||||
}
|
||||
@@ -588,6 +607,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private void addChip(@NonNull ContactChip chip) {
|
||||
chipGroup.addView(chip);
|
||||
updateGroupLimit(getChipCount());
|
||||
if (selectionWarningLimitReachedExactly()) {
|
||||
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
private int getChipCount() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChange
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@@ -62,7 +63,8 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_SMS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
|
||||
setContentView(R.layout.invite_activity);
|
||||
|
||||
49
app/src/main/java/org/thoughtcrime/securesms/KbsEnclave.java
Normal file
49
app/src/main/java/org/thoughtcrime/securesms/KbsEnclave.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Used in our {@link BuildConfig} to tie together the various attributes of a KBS instance. This
|
||||
* is sitting in the root directory so it can be accessed by the build config.
|
||||
*/
|
||||
public final class KbsEnclave {
|
||||
|
||||
private final String enclaveName;
|
||||
private final String serviceId;
|
||||
private final String mrEnclave;
|
||||
|
||||
public KbsEnclave(@NonNull String enclaveName, @NonNull String serviceId, @NonNull String mrEnclave) {
|
||||
this.enclaveName = enclaveName;
|
||||
this.serviceId = serviceId;
|
||||
this.mrEnclave = mrEnclave;
|
||||
}
|
||||
|
||||
public @NonNull String getMrEnclave() {
|
||||
return mrEnclave;
|
||||
}
|
||||
|
||||
public @NonNull String getEnclaveName() {
|
||||
return enclaveName;
|
||||
}
|
||||
|
||||
public @NonNull String getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
KbsEnclave enclave = (KbsEnclave) o;
|
||||
return enclaveName.equals(enclave.enclaveName) &&
|
||||
serviceId.equals(enclave.serviceId) &&
|
||||
mrEnclave.equals(enclave.mrEnclave);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(enclaveName, serviceId, mrEnclave);
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,14 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Menu;
|
||||
@@ -37,6 +39,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -61,6 +64,7 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -70,6 +74,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
@@ -193,7 +198,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (threadRecipient != null) {
|
||||
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
||||
if (threadRecipient.isLocalNumber()) {
|
||||
if (threadRecipient.isSelf()) {
|
||||
from = getString(R.string.note_to_self);
|
||||
} else {
|
||||
to = threadRecipient.getDisplayName(this);
|
||||
@@ -258,6 +263,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
albumRail = findViewById(R.id.media_preview_album_rail);
|
||||
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
||||
|
||||
albumRail.setItemAnimator(null); // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
|
||||
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||
albumRail.setAdapter(albumRailAdapter);
|
||||
|
||||
@@ -376,6 +382,27 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void share() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
Uri publicUri = PartAuthority.getAttachmentPublicUri(mediaItem.uri);
|
||||
String mimeType = Intent.normalizeMimeType(mediaItem.type);
|
||||
Intent shareIntent = ShareCompat.IntentBuilder.from(this)
|
||||
.setStream(publicUri)
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
try {
|
||||
startActivity(shareIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "No activity existed to share the media.", e);
|
||||
Toast.makeText(this, R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint("InlinedApi")
|
||||
private void saveToDisk() {
|
||||
@@ -383,21 +410,30 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (mediaItem != null) {
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
if (StorageUtil.canWriteToMediaStore()) {
|
||||
performSavetoDisk(mediaItem);
|
||||
return;
|
||||
}
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
||||
performSavetoDisk(mediaItem);
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void performSavetoDisk(@NonNull MediaItem mediaItem) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void deleteMedia() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
@@ -444,6 +480,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
menu.findItem(R.id.delete).setVisible(false);
|
||||
}
|
||||
|
||||
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
|
||||
menu.findItem(R.id.media_preview__share).setVisible(Build.VERSION.SDK_INT >= 26);
|
||||
|
||||
if (cameFromAllMedia) {
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
}
|
||||
@@ -453,16 +492,17 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.media_preview__overview: showOverview(); return true;
|
||||
case R.id.media_preview__forward: forward(); return true;
|
||||
case R.id.save: saveToDisk(); return true;
|
||||
case R.id.delete: deleteMedia(); return true;
|
||||
case android.R.id.home: finish(); return true;
|
||||
}
|
||||
int itemId = item.getItemId();
|
||||
|
||||
if (itemId == R.id.media_preview__overview) { showOverview(); return true; }
|
||||
if (itemId == R.id.media_preview__forward) { forward(); return true; }
|
||||
if (itemId == R.id.media_preview__share) { share(); return true; }
|
||||
if (itemId == R.id.save) { saveToDisk(); return true; }
|
||||
if (itemId == R.id.delete) { deleteMedia(); return true; }
|
||||
if (itemId == android.R.id.home) { finish(); return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
||||
super.onCreate(icicle, ready);
|
||||
|
||||
initializeToolbar();
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
@@ -56,6 +57,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
@@ -90,11 +92,13 @@ import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
|
||||
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
@@ -307,16 +311,26 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
byte[] localId;
|
||||
byte[] remoteId;
|
||||
|
||||
if (FeatureFlags.verifyV2() && recipient.resolve().getUuid().isPresent()) {
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
|
||||
Log.i(TAG, "Using UUID (version 2).");
|
||||
version = 2;
|
||||
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
|
||||
remoteId = UuidUtil.toByteArray(recipient.resolve().getUuid().get());
|
||||
} else {
|
||||
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
|
||||
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
|
||||
Log.i(TAG, "Using E164 (version 1).");
|
||||
version = 1;
|
||||
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
|
||||
remoteId = recipient.resolve().requireE164().getBytes();
|
||||
remoteId = resolved.requireE164().getBytes();
|
||||
} else {
|
||||
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getUuid().isPresent(), resolved.getE164().isPresent()));
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
|
||||
.setOnDismissListener(dialog -> requireActivity().finish())
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
this.recipient.observe(this, this::setRecipientText);
|
||||
|
||||
@@ -451,7 +451,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
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()));
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.AUDIO_CALL.getCode());
|
||||
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class AudioSlidePlayer implements SensorEventListener {
|
||||
|
||||
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
||||
|
||||
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
|
||||
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull AudioSlide slide;
|
||||
private final @NonNull Handler progressEventHandler;
|
||||
private final @NonNull AudioManager audioManager;
|
||||
private final @NonNull SensorManager sensorManager;
|
||||
private final @NonNull Sensor proximitySensor;
|
||||
private final @Nullable WakeLock wakeLock;
|
||||
|
||||
private @NonNull WeakReference<Listener> listener;
|
||||
private @Nullable SimpleExoPlayer mediaPlayer;
|
||||
private long startTime;
|
||||
|
||||
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
|
||||
@NonNull AudioSlide slide,
|
||||
@NonNull Listener listener)
|
||||
{
|
||||
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
|
||||
playing.get().setListener(listener);
|
||||
return playing.get();
|
||||
} else {
|
||||
return new AudioSlidePlayer(context, slide, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private AudioSlidePlayer(@NonNull Context context,
|
||||
@NonNull AudioSlide slide,
|
||||
@NonNull Listener listener)
|
||||
{
|
||||
this.context = context;
|
||||
this.slide = slide;
|
||||
this.listener = new WeakReference<>(listener);
|
||||
this.progressEventHandler = new ProgressEventHandler(this);
|
||||
this.audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
|
||||
this.sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
|
||||
} else {
|
||||
this.wakeLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void play(final double progress) throws IOException {
|
||||
play(progress, false);
|
||||
}
|
||||
|
||||
private void play(final double progress, boolean earpiece) throws IOException {
|
||||
if (this.mediaPlayer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (slide.getUri() == null) {
|
||||
throw new IOException("Slide has no URI!");
|
||||
}
|
||||
|
||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
|
||||
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
|
||||
this.startTime = System.currentTimeMillis();
|
||||
|
||||
mediaPlayer.prepare(createMediaSource(slide.getUri()));
|
||||
mediaPlayer.setPlayWhenReady(true);
|
||||
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(earpiece ? C.CONTENT_TYPE_SPEECH : C.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
|
||||
.build());
|
||||
mediaPlayer.addListener(new Player.EventListener() {
|
||||
|
||||
boolean started = false;
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
|
||||
switch (playbackState) {
|
||||
case Player.STATE_READY:
|
||||
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
if (mediaPlayer == null) return;
|
||||
|
||||
if (started) {
|
||||
Log.d(TAG, "Already started. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
started = true;
|
||||
|
||||
if (progress > 0) {
|
||||
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
||||
}
|
||||
|
||||
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
|
||||
|
||||
setPlaying(AudioSlidePlayer.this);
|
||||
}
|
||||
|
||||
notifyOnStart();
|
||||
progressEventHandler.sendEmptyMessage(0);
|
||||
break;
|
||||
|
||||
case Player.STATE_ENDED:
|
||||
Log.i(TAG, "onComplete");
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
mediaPlayer = null;
|
||||
|
||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
||||
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
||||
|
||||
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
|
||||
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
mediaPlayer = null;
|
||||
|
||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
||||
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private MediaSource createMediaSource(@NonNull Uri uri) {
|
||||
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
||||
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
|
||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
|
||||
|
||||
return new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
|
||||
.setExtractorsFactory(extractorsFactory)
|
||||
.createMediaSource(uri);
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
Log.i(TAG, "Stop called!");
|
||||
|
||||
removePlaying(this);
|
||||
|
||||
if (this.mediaPlayer != null) {
|
||||
this.mediaPlayer.stop();
|
||||
this.mediaPlayer.release();
|
||||
}
|
||||
|
||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
||||
|
||||
this.mediaPlayer = null;
|
||||
}
|
||||
|
||||
public synchronized static void stopAll() {
|
||||
if (playing.isPresent()) {
|
||||
playing.get().stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(@NonNull Listener listener) {
|
||||
this.listener = new WeakReference<>(listener);
|
||||
|
||||
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
|
||||
notifyOnStart();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull AudioSlide getAudioSlide() {
|
||||
return slide;
|
||||
}
|
||||
|
||||
|
||||
private Pair<Double, Integer> getProgress() {
|
||||
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
|
||||
return new Pair<>(0D, 0);
|
||||
} else {
|
||||
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
|
||||
(int) mediaPlayer.getCurrentPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnStart() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyOnStop() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyOnProgress(final double progress, final long millis) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onProgress(progress, millis);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull Listener getListener() {
|
||||
Listener listener = this.listener.get();
|
||||
|
||||
if (listener != null) return listener;
|
||||
else return new Listener() {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
@Override
|
||||
public void onStop() {}
|
||||
@Override
|
||||
public void onProgress(double progress, long millis) {}
|
||||
};
|
||||
}
|
||||
|
||||
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
|
||||
if (playing.isPresent() && playing.get() != player) {
|
||||
playing.get().notifyOnStop();
|
||||
playing.get().stop();
|
||||
}
|
||||
|
||||
playing = Optional.of(player);
|
||||
}
|
||||
|
||||
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
|
||||
if (playing.isPresent() && playing.get() == player) {
|
||||
playing = Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) return;
|
||||
if (mediaPlayer == null || mediaPlayer.getPlaybackState() != Player.STATE_READY) return;
|
||||
|
||||
int streamType;
|
||||
|
||||
if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
|
||||
streamType = AudioManager.STREAM_VOICE_CALL;
|
||||
} else {
|
||||
streamType = AudioManager.STREAM_MUSIC;
|
||||
}
|
||||
|
||||
if (streamType == AudioManager.STREAM_VOICE_CALL &&
|
||||
mediaPlayer.getAudioStreamType() != streamType &&
|
||||
!audioManager.isWiredHeadsetOn())
|
||||
{
|
||||
double position = mediaPlayer.getCurrentPosition();
|
||||
double duration = mediaPlayer.getDuration();
|
||||
double progress = position / duration;
|
||||
|
||||
if (wakeLock != null) wakeLock.acquire();
|
||||
stop();
|
||||
try {
|
||||
play(progress, true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
} else if (streamType == AudioManager.STREAM_MUSIC &&
|
||||
mediaPlayer.getAudioStreamType() != streamType &&
|
||||
System.currentTimeMillis() - startTime > 500)
|
||||
{
|
||||
if (wakeLock != null) wakeLock.release();
|
||||
stop();
|
||||
notifyOnStop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onStart();
|
||||
void onStop();
|
||||
void onProgress(double progress, long millis);
|
||||
}
|
||||
|
||||
private static class ProgressEventHandler extends Handler {
|
||||
|
||||
private final WeakReference<AudioSlidePlayer> playerReference;
|
||||
|
||||
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
|
||||
this.playerReference = new WeakReference<>(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
AudioSlidePlayer player = playerReference.get();
|
||||
|
||||
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pair<Double, Integer> progress = player.getProgress();
|
||||
player.notifyOnProgress(progress.first, progress.second);
|
||||
sendEmptyMessageDelayed(0, 50);
|
||||
}
|
||||
|
||||
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
|
||||
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ package org.thoughtcrime.securesms.backup;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -13,10 +17,14 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
@@ -26,25 +34,49 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
public class BackupDialog {
|
||||
|
||||
public static void showEnableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
||||
private static final String TAG = Log.tag(BackupDialog.class);
|
||||
|
||||
public static void showEnableBackupDialog(@NonNull Context context,
|
||||
@Nullable Intent backupDirectorySelectionIntent,
|
||||
@Nullable String backupDirectoryDisplayName,
|
||||
@NonNull Runnable onBackupsEnabled)
|
||||
{
|
||||
String[] password = BackupUtil.generateBackupPassphrase();
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
||||
.setView(R.layout.backup_enable_dialog)
|
||||
.setView(backupDirectorySelectionIntent != null ? R.layout.backup_enable_dialog_v29 : R.layout.backup_enable_dialog)
|
||||
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(created -> {
|
||||
if (backupDirectoryDisplayName != null) {
|
||||
TextView folderName = dialog.findViewById(R.id.backup_enable_dialog_folder_name);
|
||||
if (folderName != null) {
|
||||
folderName.setText(backupDirectoryDisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(v -> {
|
||||
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
||||
if (confirmationCheckBox.isChecked()) {
|
||||
if (backupDirectorySelectionIntent != null && backupDirectorySelectionIntent.getData() != null) {
|
||||
Uri backupDirectoryUri = backupDirectorySelectionIntent.getData();
|
||||
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
||||
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
|
||||
context.getContentResolver()
|
||||
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
|
||||
}
|
||||
|
||||
BackupPassphrase.set(context, Util.join(password, " "));
|
||||
TextSecurePreferences.setNextBackupTime(context, 0);
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
LocalBackupListener.schedule(context);
|
||||
|
||||
preference.setChecked(true);
|
||||
onBackupsEnabled.run();
|
||||
created.dismiss();
|
||||
} else {
|
||||
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
||||
@@ -75,16 +107,42 @@ public class BackupDialog {
|
||||
|
||||
}
|
||||
|
||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
||||
@RequiresApi(29)
|
||||
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) {
|
||||
new AlertDialog.Builder(fragment.requireContext())
|
||||
.setView(R.layout.backup_choose_location_dialog)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setPositiveButton(R.string.BackupDialog_choose_folder, ((dialog, which) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().getLatestSignalBackupDirectory());
|
||||
}
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
fragment.startActivityForResult(intent, requestCode);
|
||||
|
||||
dialog.dismiss();
|
||||
}))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_delete_backups)
|
||||
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
|
||||
BackupPassphrase.set(context, null);
|
||||
TextSecurePreferences.setBackupEnabled(context, false);
|
||||
BackupUtil.deleteAllBackups();
|
||||
preference.setChecked(false);
|
||||
BackupUtil.disableBackups(context);
|
||||
|
||||
onBackupsDisabled.run();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public enum BackupFileIOError {
|
||||
ACCESS_ERROR(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved),
|
||||
FILE_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_file_is_too_large),
|
||||
NOT_ENOUGH_SPACE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_there_is_not_enough_space),
|
||||
UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups);
|
||||
|
||||
private static final short BACKUP_FAILED_ID = 31321;
|
||||
|
||||
private final @StringRes int titleId;
|
||||
private final @StringRes int messageId;
|
||||
|
||||
BackupFileIOError(@StringRes int titleId, @StringRes int messageId) {
|
||||
this.titleId = titleId;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public static void clearNotification(@NonNull Context context) {
|
||||
NotificationManagerCompat.from(context).cancel(BACKUP_FAILED_ID);
|
||||
}
|
||||
|
||||
public void postNotification(@NonNull Context context) {
|
||||
Intent intent = new Intent(context, ApplicationPreferencesActivity.class);
|
||||
|
||||
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_BACKUPS_FRAGMENT, true);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, intent, 0);
|
||||
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
|
||||
.setSmallIcon(R.drawable.ic_signal_backup)
|
||||
.setContentTitle(context.getString(titleId))
|
||||
.setContentText(context.getString(messageId))
|
||||
.setContentIntent(pendingIntent)
|
||||
.build();
|
||||
|
||||
NotificationManagerCompat.from(context)
|
||||
.notify(BACKUP_FAILED_ID, backupFailedNotification);
|
||||
}
|
||||
|
||||
public static void postNotificationForException(@NonNull Context context, @NonNull IOException e, int runAttempt) {
|
||||
BackupFileIOError error = getFromException(e);
|
||||
|
||||
if (error != null) {
|
||||
error.postNotification(context);
|
||||
}
|
||||
|
||||
if (error == null && runAttempt > 0) {
|
||||
UNKNOWN.postNotification(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable BackupFileIOError getFromException(@NonNull IOException e) {
|
||||
if (e.getMessage() != null) {
|
||||
if (e.getMessage().contains("EFBIG")) return FILE_TOO_LARGE;
|
||||
else if (e.getMessage().contains("ENOSPC")) return NOT_ENOUGH_SPACE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.backup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.annimon.stream.function.Consumer;
|
||||
import com.annimon.stream.function.Predicate;
|
||||
@@ -50,6 +53,7 @@ import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
@@ -84,7 +88,32 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase);
|
||||
try (OutputStream outputStream = new FileOutputStream(output)) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
public static void export(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull DocumentFile output,
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
try (OutputStream outputStream = Objects.requireNonNull(context.getContentResolver().openOutputStream(output.getUri()))) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
private static void internalExport(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull OutputStream fileOutputStream,
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
|
||||
int count = 0;
|
||||
|
||||
try {
|
||||
@@ -322,7 +351,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
private byte[] iv;
|
||||
private int counter;
|
||||
|
||||
private BackupFrameOutputStream(@NonNull File output, @NonNull String passphrase) throws IOException {
|
||||
private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException {
|
||||
try {
|
||||
byte[] salt = Util.getSecretBytes(32);
|
||||
byte[] key = getBackupKey(passphrase, salt);
|
||||
@@ -334,7 +363,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
this.mac = Mac.getInstance("HmacSHA256");
|
||||
this.outputStream = new FileOutputStream(output);
|
||||
this.outputStream = output;
|
||||
this.iv = Util.getSecretBytes(16);
|
||||
this.counter = Conversions.byteArrayToInt(iv);
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
@@ -25,8 +27,8 @@ import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -36,7 +38,6 @@ import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@@ -46,6 +47,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
@@ -61,13 +63,14 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static final String TAG = FullBackupImporter.class.getSimpleName();
|
||||
|
||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase db, @NonNull File file, @NonNull String passphrase)
|
||||
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
BackupRecordInputStream inputStream = new BackupRecordInputStream(file, passphrase);
|
||||
int count = 0;
|
||||
int count = 0;
|
||||
|
||||
try (InputStream is = getInputStream(context, uri)) {
|
||||
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
|
||||
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
dropAllTables(db);
|
||||
@@ -93,6 +96,14 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
|
||||
}
|
||||
|
||||
private static @NonNull InputStream getInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException{
|
||||
if (BackupUtil.isUserSelectionRequired(context)) {
|
||||
return Objects.requireNonNull(context.getContentResolver().openInputStream(uri));
|
||||
} else {
|
||||
return new FileInputStream(new File(Objects.requireNonNull(uri.getPath())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) throws IOException {
|
||||
if (version.getVersion() > db.getVersion()) {
|
||||
throw new DatabaseDowngradeException(db.getVersion(), version.getVersion());
|
||||
@@ -221,9 +232,9 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private byte[] iv;
|
||||
private int counter;
|
||||
|
||||
private BackupRecordInputStream(@NonNull File file, @NonNull String passphrase) throws IOException {
|
||||
private BackupRecordInputStream(@NonNull InputStream in, @NonNull String passphrase) throws IOException {
|
||||
try {
|
||||
this.in = new FileInputStream(file);
|
||||
this.in = in;
|
||||
|
||||
byte[] headerLengthBytes = new byte[4];
|
||||
Util.readFully(in, headerLengthBytes);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.color;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
@@ -8,6 +9,7 @@ import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -68,6 +70,11 @@ public enum MaterialColor {
|
||||
this.serialized = serialized;
|
||||
}
|
||||
|
||||
public @ColorInt int toNotificationColor(@NonNull Context context) {
|
||||
final boolean isDark = ThemeUtil.isDarkNotificationTheme(context);
|
||||
return context.getResources().getColor(isDark ? shadeColor : mainColor);
|
||||
}
|
||||
|
||||
public @ColorInt int toConversationColor(@NonNull Context context) {
|
||||
return context.getResources().getColor(mainColor);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
@@ -16,6 +17,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.airbnb.lottie.LottieProperty;
|
||||
@@ -28,19 +30,17 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.audio.AudioWaveForm;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
|
||||
public final class AudioView extends FrameLayout {
|
||||
|
||||
private static final String TAG = AudioView.class.getSimpleName();
|
||||
|
||||
@@ -60,13 +60,17 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
|
||||
@ColorInt private final int waveFormPlayedBarsColor;
|
||||
@ColorInt private final int waveFormUnplayedBarsColor;
|
||||
@ColorInt private final int waveFormThumbTint;
|
||||
|
||||
@Nullable private SlideClickListener downloadListener;
|
||||
@Nullable private AudioSlidePlayer audioSlidePlayer;
|
||||
private int backwardsCounter;
|
||||
private int lottieDirection;
|
||||
private boolean isPlaying;
|
||||
private long durationMillis;
|
||||
private AudioSlide audioSlide;
|
||||
private Callbacks callbacks;
|
||||
|
||||
private final Observer<VoiceNotePlaybackState> playbackStateObserver = this::onPlaybackState;
|
||||
|
||||
public AudioView(Context context) {
|
||||
this(context, null);
|
||||
@@ -103,6 +107,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
|
||||
this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
|
||||
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
|
||||
this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);
|
||||
|
||||
progressAndPlay.getBackground().setColorFilter(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK), PorterDuff.Mode.SRC_IN);
|
||||
} finally {
|
||||
if (typedArray != null) {
|
||||
typedArray.recycle();
|
||||
@@ -122,11 +129,23 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
public Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
|
||||
return playbackStateObserver;
|
||||
}
|
||||
|
||||
public void setAudio(final @NonNull AudioSlide audio,
|
||||
final boolean showControls)
|
||||
final @Nullable Callbacks callbacks,
|
||||
final boolean showControls,
|
||||
final boolean forceHideDuration)
|
||||
{
|
||||
this.callbacks = callbacks;
|
||||
|
||||
if (duration != null) {
|
||||
duration.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (seekBar instanceof WaveFormSeekBarView) {
|
||||
if (audioSlidePlayer != null && !Objects.equals(audioSlidePlayer.getAudioSlide().getUri(), audio.getUri())) {
|
||||
if (audioSlide != null && !Objects.equals(audioSlide.getUri(), audio.getUri())) {
|
||||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||
waveFormView.setWaveMode(false);
|
||||
seekBar.setProgress(0);
|
||||
@@ -139,30 +158,29 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
seekBar.setEnabled(false);
|
||||
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
|
||||
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||
circleProgress.setVisibility(View.GONE);
|
||||
} else if (showControls && audio.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
|
||||
controlToggle.displayQuick(progressAndPlay);
|
||||
seekBar.setEnabled(false);
|
||||
circleProgress.setVisibility(View.VISIBLE);
|
||||
circleProgress.spin();
|
||||
} else {
|
||||
seekBar.setEnabled(true);
|
||||
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||
showPlayButton();
|
||||
lottieDirection = REVERSE;
|
||||
playPauseButton.cancelAnimation();
|
||||
playPauseButton.setFrame(0);
|
||||
}
|
||||
|
||||
this.audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
|
||||
this.audioSlide = audio;
|
||||
|
||||
if (seekBar instanceof WaveFormSeekBarView) {
|
||||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor);
|
||||
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor, waveFormThumbTint);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
new AudioWaveForm(getContext(), audio).getWaveForm(
|
||||
data -> {
|
||||
if (duration != null) {
|
||||
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||
updateProgress(0, 0);
|
||||
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||
updateProgress(0, 0);
|
||||
if (!forceHideDuration && duration != null) {
|
||||
duration.setVisibility(VISIBLE);
|
||||
}
|
||||
waveFormView.setWaveData(data.getWaveForm());
|
||||
@@ -175,11 +193,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
if (this.audioSlidePlayer != null && isPlaying) {
|
||||
this.audioSlidePlayer.stop();
|
||||
if (forceHideDuration && duration != null) {
|
||||
duration.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,23 +203,84 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
this.downloadListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
public @Nullable Uri getAudioSlideUri() {
|
||||
if (audioSlide != null) return audioSlide.getUri();
|
||||
else return null;
|
||||
}
|
||||
|
||||
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
|
||||
onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
|
||||
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
|
||||
onProgress(voiceNotePlaybackState.getUri(),
|
||||
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
|
||||
voiceNotePlaybackState.getPlayheadPositionMillis());
|
||||
}
|
||||
|
||||
private void onDuration(@NonNull Uri uri, long durationMillis) {
|
||||
if (isTarget(uri)) {
|
||||
this.durationMillis = durationMillis;
|
||||
}
|
||||
}
|
||||
|
||||
private void onStart(@NonNull Uri uri, boolean autoReset) {
|
||||
if (!isTarget(uri)) {
|
||||
if (hasAudioUri()) {
|
||||
onStop(audioSlide.getUri(), autoReset);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPlaying = true;
|
||||
togglePlayToPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
private void onStop(@NonNull Uri uri, boolean autoReset) {
|
||||
if (!isTarget(uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPlaying = false;
|
||||
togglePauseToPlay();
|
||||
|
||||
if (autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
|
||||
if (autoReset || autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
|
||||
backwardsCounter = 4;
|
||||
rewind();
|
||||
}
|
||||
}
|
||||
|
||||
private void onProgress(@NonNull Uri uri, double progress, long millis) {
|
||||
if (!isTarget(uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int seekProgress = (int) Math.floor(progress * seekBar.getMax());
|
||||
|
||||
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
|
||||
backwardsCounter = 0;
|
||||
seekBar.setProgress(seekProgress);
|
||||
updateProgress((float) progress, millis);
|
||||
} else {
|
||||
backwardsCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTarget(@NonNull Uri uri) {
|
||||
return hasAudioUri() && Objects.equals(uri, audioSlide.getUri());
|
||||
}
|
||||
|
||||
private boolean hasAudioUri() {
|
||||
return audioSlide != null && audioSlide.getUri() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocusable(boolean focusable) {
|
||||
super.setFocusable(focusable);
|
||||
@@ -230,20 +307,11 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
this.downloadButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(double progress, long millis) {
|
||||
int seekProgress = (int) Math.floor(progress * seekBar.getMax());
|
||||
|
||||
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
|
||||
backwardsCounter = 0;
|
||||
seekBar.setProgress(seekProgress);
|
||||
updateProgress((float) progress, millis);
|
||||
} else {
|
||||
backwardsCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(float progress, long millis) {
|
||||
if (callbacks != null) {
|
||||
callbacks.onProgressUpdated(durationMillis, millis);
|
||||
}
|
||||
|
||||
if (duration != null && durationMillis > 0) {
|
||||
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
|
||||
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||
@@ -306,41 +374,31 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
if (!smallView || seekBar.getProgress() == 0) {
|
||||
circleProgress.setInstantProgress(1);
|
||||
}
|
||||
circleProgress.setVisibility(VISIBLE);
|
||||
circleProgress.setVisibility(GONE);
|
||||
playPauseButton.setVisibility(VISIBLE);
|
||||
controlToggle.displayQuick(progressAndPlay);
|
||||
}
|
||||
|
||||
public void stopPlaybackAndReset() {
|
||||
if (this.audioSlidePlayer != null && isPlaying) {
|
||||
this.audioSlidePlayer.stop();
|
||||
togglePauseToPlay();
|
||||
if (audioSlide == null || audioSlide.getUri() == null) return;
|
||||
|
||||
if (callbacks != null) {
|
||||
callbacks.onStopAndReset(audioSlide.getUri());
|
||||
rewind();
|
||||
}
|
||||
rewind();
|
||||
}
|
||||
|
||||
private class PlayPauseClickedListener implements View.OnClickListener {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (lottieDirection == REVERSE) {
|
||||
try {
|
||||
Log.d(TAG, "playbutton onClick");
|
||||
if (audioSlidePlayer != null) {
|
||||
togglePlayToPause();
|
||||
audioSlidePlayer.play(getProgress());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "pausebutton onClick");
|
||||
if (audioSlidePlayer != null) {
|
||||
togglePauseToPlay();
|
||||
audioSlidePlayer.stop();
|
||||
if (autoRewind) {
|
||||
rewind();
|
||||
}
|
||||
if (audioSlide == null || audioSlide.getUri() == null) return;
|
||||
|
||||
if (callbacks != null) {
|
||||
if (lottieDirection == REVERSE) {
|
||||
callbacks.onPlay(audioSlide.getUri(), getProgress());
|
||||
} else {
|
||||
callbacks.onPause(audioSlide.getUri());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,28 +428,28 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser && durationMillis > 0) {
|
||||
float progressFloat = progress / (float) seekBar.getMax();
|
||||
updateProgress(progressFloat, (long) (durationMillis * progressFloat));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
|
||||
if (audioSlide == null || audioSlide.getUri() == null) return;
|
||||
|
||||
wasPlaying = isPlaying;
|
||||
if (audioSlidePlayer != null && isPlaying) {
|
||||
audioSlidePlayer.stop();
|
||||
if (isPlaying) {
|
||||
if (callbacks != null) {
|
||||
callbacks.onPause(audioSlide.getUri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStopTrackingTouch(SeekBar seekBar) {
|
||||
try {
|
||||
if (audioSlidePlayer != null && wasPlaying) {
|
||||
audioSlidePlayer.play(getProgress());
|
||||
if (audioSlide == null || audioSlide.getUri() == null) return;
|
||||
|
||||
if (callbacks != null) {
|
||||
if (wasPlaying) {
|
||||
callbacks.onSeekTo(audioSlide.getUri(), getProgress());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,9 +463,16 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventAsync(final PartProgressEvent event) {
|
||||
if (audioSlidePlayer != null && event.attachment.equals(audioSlidePlayer.getAudioSlide().asAttachment())) {
|
||||
if (audioSlide != null && event.attachment.equals(audioSlide.asAttachment())) {
|
||||
circleProgress.setInstantProgress(((float) event.progress) / event.total);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callbacks {
|
||||
void onPlay(@NonNull Uri audioUri, double progress);
|
||||
void onPause(@NonNull Uri audioUri);
|
||||
void onSeekTo(@NonNull Uri audioUri, double progress);
|
||||
void onStopAndReset(@NonNull Uri audioUri);
|
||||
void onProgressUpdated(long durationMillis, long playheadMillis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
* Shows self as the actual profile picture.
|
||||
*/
|
||||
public void setRecipient(@NonNull Recipient recipient) {
|
||||
if (recipient.isLocalNumber()) {
|
||||
if (recipient.isSelf()) {
|
||||
setAvatar(GlideApp.with(this), null, false);
|
||||
AvatarUtil.loadIconIntoImageView(recipient, this);
|
||||
} else {
|
||||
|
||||
@@ -100,7 +100,7 @@ public class ComposeText extends EmojiEditText {
|
||||
protected void onSelectionChanged(int selectionStart, int selectionEnd) {
|
||||
super.onSelectionChanged(selectionStart, selectionEnd);
|
||||
|
||||
if (FeatureFlags.mentions() && getText() != null) {
|
||||
if (getText() != null) {
|
||||
boolean selectionChanged = changeSelectionForPartialMentions(getText(), selectionStart, selectionEnd);
|
||||
if (selectionChanged) {
|
||||
return;
|
||||
@@ -192,9 +192,7 @@ public class ComposeText extends EmojiEditText {
|
||||
}
|
||||
|
||||
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
|
||||
if (FeatureFlags.mentions()) {
|
||||
mentionValidatorWatcher.setMentionValidator(mentionValidator);
|
||||
}
|
||||
mentionValidatorWatcher.setMentionValidator(mentionValidator);
|
||||
}
|
||||
|
||||
private boolean isLandscape() {
|
||||
@@ -261,11 +259,9 @@ public class ComposeText extends EmojiEditText {
|
||||
|
||||
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ThemeUtil.getThemedColor(getContext(), R.attr.conversation_mention_background_color));
|
||||
|
||||
if (FeatureFlags.mentions()) {
|
||||
addTextChangedListener(new MentionDeleter());
|
||||
mentionValidatorWatcher = new MentionValidatorWatcher();
|
||||
addTextChangedListener(mentionValidatorWatcher);
|
||||
}
|
||||
addTextChangedListener(new MentionDeleter());
|
||||
mentionValidatorWatcher = new MentionValidatorWatcher();
|
||||
addTextChangedListener(mentionValidatorWatcher);
|
||||
}
|
||||
|
||||
private boolean changeSelectionForPartialMentions(@NonNull Spanned spanned, int selectionStart, int selectionEnd) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -15,18 +16,26 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.airbnb.lottie.LottieProperty;
|
||||
import com.airbnb.lottie.model.KeyPath;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ConversationItemFooter extends LinearLayout {
|
||||
|
||||
@@ -35,6 +44,10 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
private ExpirationTimerView timerView;
|
||||
private ImageView insecureIndicatorView;
|
||||
private DeliveryStatusView deliveryStatusView;
|
||||
private boolean onlyShowSendingStatus;
|
||||
private View audioSpace;
|
||||
private TextView audioDuration;
|
||||
private LottieAnimationView revealDot;
|
||||
|
||||
public ConversationItemFooter(Context context) {
|
||||
super(context);
|
||||
@@ -59,11 +72,15 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
timerView = findViewById(R.id.footer_expiration_timer);
|
||||
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
|
||||
deliveryStatusView = findViewById(R.id.footer_delivery_status);
|
||||
audioDuration = findViewById(R.id.footer_audio_duration);
|
||||
audioSpace = findViewById(R.id.footer_audio_duration_space);
|
||||
revealDot = findViewById(R.id.footer_revealed_dot);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
|
||||
setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white)));
|
||||
setIconColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_icon_color, getResources().getColor(R.color.core_white)));
|
||||
setRevealDotColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_reveal_dot_color, getResources().getColor(R.color.core_white)));
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
@@ -80,11 +97,18 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
presentTimer(messageRecord);
|
||||
presentInsecureIndicator(messageRecord);
|
||||
presentDeliveryStatus(messageRecord);
|
||||
hideAudioDurationViews();
|
||||
}
|
||||
|
||||
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
|
||||
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis);
|
||||
audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||
}
|
||||
|
||||
public void setTextColor(int color) {
|
||||
dateView.setTextColor(color);
|
||||
simView.setTextColor(color);
|
||||
audioDuration.setTextColor(color);
|
||||
}
|
||||
|
||||
public void setIconColor(int color) {
|
||||
@@ -93,6 +117,19 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
deliveryStatusView.setTint(color);
|
||||
}
|
||||
|
||||
public void setRevealDotColor(int color) {
|
||||
revealDot.addValueCallback(
|
||||
new KeyPath("**"),
|
||||
LottieProperty.COLOR_FILTER,
|
||||
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||
);
|
||||
}
|
||||
|
||||
public void setOnlyShowSendingStatus(boolean onlyShowSending, MessageRecord messageRecord) {
|
||||
this.onlyShowSendingStatus = onlyShowSending;
|
||||
presentDeliveryStatus(messageRecord);
|
||||
}
|
||||
|
||||
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
||||
dateView.forceLayout();
|
||||
if (messageRecord.isFailed()) {
|
||||
@@ -173,14 +210,89 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
}
|
||||
|
||||
private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
|
||||
if (!messageRecord.isFailed() && !messageRecord.isPendingInsecureSmsFallback()) {
|
||||
if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
|
||||
else if (messageRecord.isPending()) deliveryStatusView.setPending();
|
||||
else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead();
|
||||
else if (messageRecord.isDelivered()) deliveryStatusView.setDelivered();
|
||||
else deliveryStatusView.setSent();
|
||||
} else {
|
||||
if (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback()) {
|
||||
deliveryStatusView.setNone();
|
||||
return;
|
||||
}
|
||||
|
||||
if (onlyShowSendingStatus) {
|
||||
if (messageRecord.isOutgoing() && messageRecord.isPending()) {
|
||||
deliveryStatusView.setPending();
|
||||
} else {
|
||||
deliveryStatusView.setNone();
|
||||
}
|
||||
} else {
|
||||
if (!messageRecord.isOutgoing()) {
|
||||
deliveryStatusView.setNone();
|
||||
} else if (messageRecord.isPending()) {
|
||||
deliveryStatusView.setPending();
|
||||
} else if (messageRecord.isRemoteRead()) {
|
||||
deliveryStatusView.setRead();
|
||||
} else if (messageRecord.isDelivered()) {
|
||||
deliveryStatusView.setDelivered();
|
||||
} else {
|
||||
deliveryStatusView.setSent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void presentAudioDuration(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isMms()) {
|
||||
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
|
||||
|
||||
if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
moveAudioViewsForOutgoing();
|
||||
} else {
|
||||
moveAudioViewsForIncoming();
|
||||
}
|
||||
showAudioDurationViews();
|
||||
} else {
|
||||
hideAudioDurationViews();
|
||||
}
|
||||
} else {
|
||||
hideAudioDurationViews();
|
||||
}
|
||||
}
|
||||
|
||||
private void moveAudioViewsForOutgoing() {
|
||||
removeView(audioSpace);
|
||||
removeView(audioDuration);
|
||||
removeView(revealDot);
|
||||
addView(audioSpace, 0);
|
||||
addView(revealDot, 0);
|
||||
addView(audioDuration, 0);
|
||||
|
||||
int padStart = ViewUtil.dpToPx(60);
|
||||
int padLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? padStart : 0;
|
||||
int padRight = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? padStart : 0;
|
||||
|
||||
audioDuration.setPadding(padLeft, 0, padRight, 0);
|
||||
}
|
||||
|
||||
private void moveAudioViewsForIncoming() {
|
||||
removeView(audioSpace);
|
||||
removeView(audioDuration);
|
||||
removeView(revealDot);
|
||||
addView(audioSpace);
|
||||
addView(revealDot);
|
||||
addView(audioDuration);
|
||||
|
||||
audioDuration.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private void showAudioDurationViews() {
|
||||
audioSpace.setVisibility(View.VISIBLE);
|
||||
audioDuration.setVisibility(View.VISIBLE);
|
||||
|
||||
if (FeatureFlags.viewedReceipts()) {
|
||||
revealDot.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideAudioDurationViews() {
|
||||
audioSpace.setVisibility(View.GONE);
|
||||
audioDuration.setVisibility(View.GONE);
|
||||
revealDot.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,16 @@ import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
import org.thoughtcrime.securesms.util.spans.CenterAlignedRelativeSizeSpan;
|
||||
|
||||
public class FromTextView extends EmojiTextView {
|
||||
|
||||
@@ -59,7 +53,7 @@ public class FromTextView extends EmojiTextView {
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
if (recipient.isSelf()) {
|
||||
builder.append(getContext().getString(R.string.note_to_self));
|
||||
} else {
|
||||
builder.append(fromSpan);
|
||||
|
||||
@@ -32,7 +32,7 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
public final @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
|
||||
inflater.inflate(getDialogLayoutResource(), view.findViewById(R.id.full_screen_dialog_content), true);
|
||||
toolbar = view.findViewById(R.id.full_screen_dialog_toolbar);
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.appcompat.widget.LinearLayoutCompat;
|
||||
|
||||
import android.util.AttributeSet;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.Surface;
|
||||
@@ -31,6 +32,7 @@ import android.view.View;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashSet;
|
||||
@@ -69,17 +71,17 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
|
||||
public KeyboardAwareLinearLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
final int statusBarRes = getResources().getIdentifier("status_bar_height", "dimen", "android");
|
||||
minKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
|
||||
minCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
|
||||
defaultCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
|
||||
minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
statusBarHeight = statusBarRes > 0 ? getResources().getDimensionPixelSize(statusBarRes) : 0;
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
viewInset = getViewInset();
|
||||
}
|
||||
|
||||
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
updateRotation();
|
||||
updateKeyboardState();
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
@@ -100,7 +102,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
getWindowVisibleDisplayFrame(rect);
|
||||
|
||||
final int availableHeight = getAvailableHeight();
|
||||
final int keyboardHeight = availableHeight - (rect.bottom - rect.top);
|
||||
final int keyboardHeight = availableHeight - rect.bottom;
|
||||
|
||||
if (keyboardHeight > minKeyboardSize) {
|
||||
if (getKeyboardHeight() != keyboardHeight) {
|
||||
@@ -128,19 +130,19 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
|
||||
stableInsetsField.setAccessible(true);
|
||||
Rect insets = (Rect)stableInsetsField.get(attachInfo);
|
||||
return insets.bottom;
|
||||
if (insets != null) {
|
||||
return insets.bottom;
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFieldException nsfe) {
|
||||
Log.w(TAG, "field reflection error when measuring view inset", nsfe);
|
||||
} catch (IllegalAccessException iae) {
|
||||
Log.w(TAG, "access reflection error when measuring view inset", iae);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
// Do nothing
|
||||
}
|
||||
return 0;
|
||||
return statusBarHeight;
|
||||
}
|
||||
|
||||
private int getAvailableHeight() {
|
||||
final int availableHeight = this.getRootView().getHeight() - viewInset - (!isFullscreen ? statusBarHeight : 0);
|
||||
final int availableWidth = this.getRootView().getWidth() - (!isFullscreen ? statusBarHeight : 0);
|
||||
final int availableHeight = this.getRootView().getHeight() - viewInset;
|
||||
final int availableWidth = this.getRootView().getWidth();
|
||||
|
||||
if (isLandscape() && availableHeight > availableWidth) {
|
||||
//noinspection SuspiciousNameCombination
|
||||
|
||||
@@ -190,8 +190,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
private void setQuoteAuthor(@NonNull Recipient author) {
|
||||
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
||||
|
||||
authorView.setText(author.isLocalNumber() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
|
||||
// We use the raw color resource because Android 4.x was struggling with tints here
|
||||
quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components;
|
||||
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
@@ -106,7 +107,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
viewHolder.imageView.setImageDrawable(null);
|
||||
|
||||
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA));
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
|
||||
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
|
||||
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
|
||||
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
|
||||
@@ -116,7 +117,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
|
||||
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
|
||||
|
||||
final Uri uri = Uri.fromFile(new File(path));
|
||||
final Uri uri = ContentUris.withAppendedId(RecentPhotosLoader.BASE_URL, rowId);
|
||||
|
||||
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public class TypingStatusRepository {
|
||||
}
|
||||
|
||||
public synchronized void onTypingStarted(@NonNull Context context, long threadId, @NonNull Recipient author, int device) {
|
||||
if (author.isLocalNumber()) {
|
||||
if (author.isSelf()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class TypingStatusRepository {
|
||||
}
|
||||
|
||||
public synchronized void onTypingStopped(@NonNull Context context, long threadId, @NonNull Recipient author, int device, boolean isReplacedByIncomingMessage) {
|
||||
if (author.isLocalNumber()) {
|
||||
if (author.isSelf()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.Interpolator;
|
||||
@@ -13,6 +14,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.appcompat.widget.AppCompatSeekBar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
@@ -65,9 +67,12 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
|
||||
barWidth = getResources().getDimensionPixelSize(R.dimen.wave_form_bar_width);
|
||||
}
|
||||
|
||||
public void setColors(@ColorInt int playedBarColor, @ColorInt int unplayedBarColor) {
|
||||
public void setColors(@ColorInt int playedBarColor, @ColorInt int unplayedBarColor, @ColorInt int thumbTint) {
|
||||
this.playedBarColor = playedBarColor;
|
||||
this.unplayedBarColor = unplayedBarColor;
|
||||
|
||||
getThumb().setColorFilter(thumbTint, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class WarningTextView extends AppCompatTextView {
|
||||
|
||||
@ColorInt private final int originalTextColor;
|
||||
@ColorInt private final int warningTextColor;
|
||||
|
||||
private boolean warning;
|
||||
|
||||
public WarningTextView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public WarningTextView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public WarningTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.WarningTextView, 0, 0);
|
||||
warningTextColor = styledAttributes.getColor(R.styleable.WarningTextView_warning_text_color, 0);
|
||||
|
||||
styledAttributes.recycle();
|
||||
|
||||
styledAttributes = context.obtainStyledAttributes(attrs, new int[]{ android.R.attr.textColor });
|
||||
|
||||
originalTextColor = styledAttributes.getColor(0, 0);
|
||||
|
||||
styledAttributes.recycle();
|
||||
}
|
||||
|
||||
public void setWarning(boolean warning) {
|
||||
if (this.warning != warning) {
|
||||
this.warning = warning;
|
||||
setTextColor(warning ? warningTextColor : originalTextColor);
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,34 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.provider.Telephony;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.SmsUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class DefaultSmsReminder extends Reminder {
|
||||
|
||||
@TargetApi(VERSION_CODES.KITKAT)
|
||||
public DefaultSmsReminder(final Context context) {
|
||||
super(context.getString(R.string.reminder_header_sms_default_title),
|
||||
context.getString(R.string.reminder_header_sms_default_text));
|
||||
public DefaultSmsReminder(@NonNull Fragment fragment, short requestCode) {
|
||||
super(fragment.getString(R.string.reminder_header_sms_default_title),
|
||||
fragment.getString(R.string.reminder_header_sms_default_text));
|
||||
|
||||
final OnClickListener okListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
TextSecurePreferences.setPromptedDefaultSmsProvider(context, true);
|
||||
Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT);
|
||||
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, context.getPackageName());
|
||||
context.startActivity(intent);
|
||||
TextSecurePreferences.setPromptedDefaultSmsProvider(fragment.requireContext(), true);
|
||||
fragment.startActivityForResult(SmsUtil.getSmsRoleIntent(fragment.requireContext()), requestCode);
|
||||
}
|
||||
};
|
||||
final OnClickListener dismissListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
TextSecurePreferences.setPromptedDefaultSmsProvider(context, true);
|
||||
TextSecurePreferences.setPromptedDefaultSmsProvider(fragment.requireContext(), true);
|
||||
}
|
||||
};
|
||||
setOkListener(okListener);
|
||||
|
||||
@@ -23,7 +23,12 @@ public class OutdatedBuildReminder extends Reminder {
|
||||
|
||||
private static CharSequence getPluralsText(final Context context) {
|
||||
int days = getDaysUntilExpiry() - 1;
|
||||
return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days);
|
||||
|
||||
if (days == 0) {
|
||||
return context.getString(R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today);
|
||||
} else {
|
||||
return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shown to admins when there are pending group join requests.
|
||||
*/
|
||||
public final class PendingGroupJoinRequestsReminder extends Reminder {
|
||||
|
||||
private PendingGroupJoinRequestsReminder(@Nullable CharSequence title,
|
||||
@NonNull CharSequence text)
|
||||
{
|
||||
super(title, text);
|
||||
}
|
||||
|
||||
public static Reminder create(@NonNull Context context, int count) {
|
||||
String message = context.getResources().getQuantityString(R.plurals.PendingGroupJoinRequestsReminder_d_pending_member_requests, count, count);
|
||||
Reminder reminder = new PendingGroupJoinRequestsReminder(null, message);
|
||||
|
||||
reminder.addAction(new Action(context.getString(R.string.PendingGroupJoinRequestsReminder_view), R.id.reminder_action_review_join_requests));
|
||||
|
||||
return reminder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDismissable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Importance getImportance() {
|
||||
return Importance.NORMAL;
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ public abstract class Reminder {
|
||||
NORMAL, ERROR, TERMINAL
|
||||
}
|
||||
|
||||
public final class Action {
|
||||
public static final class Action {
|
||||
private final CharSequence title;
|
||||
private final int actionId;
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Encapsulates control of voice note playback from an Activity component.
|
||||
*
|
||||
* This class assumes that it will be created within the scope of Activity#onCreate
|
||||
*
|
||||
* The workhorse of this repository is the ProgressEventHandler, which will supply a
|
||||
* steady stream of update events to the set callback.
|
||||
*/
|
||||
public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
|
||||
public static final String EXTRA_MESSAGE_ID = "voice.note.message_id";
|
||||
public static final String EXTRA_PROGRESS = "voice.note.playhead";
|
||||
public static final String EXTRA_PLAY_SINGLE = "voice.note.play.single";
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteMediaController.class);
|
||||
|
||||
private MediaBrowserCompat mediaBrowser;
|
||||
private AppCompatActivity activity;
|
||||
private ProgressEventHandler progressEventHandler;
|
||||
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
|
||||
|
||||
private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback();
|
||||
|
||||
public VoiceNoteMediaController(@NonNull AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
this.mediaBrowser = new MediaBrowserCompat(activity,
|
||||
new ComponentName(activity, VoiceNotePlaybackService.class),
|
||||
new ConnectionCallback(),
|
||||
null);
|
||||
|
||||
activity.getLifecycle().addObserver(this);
|
||||
}
|
||||
|
||||
public LiveData<VoiceNotePlaybackState> getVoiceNotePlaybackState() {
|
||||
return voiceNotePlaybackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
mediaBrowser.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(@NonNull LifecycleOwner owner) {
|
||||
activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
clearProgressEventHandler();
|
||||
|
||||
if (MediaControllerCompat.getMediaController(activity) != null) {
|
||||
MediaControllerCompat.getMediaController(activity).unregisterCallback(mediaControllerCompatCallback);
|
||||
}
|
||||
mediaBrowser.disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(@NonNull LifecycleOwner owner) {
|
||||
activity.getLifecycle().removeObserver(this);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
private static boolean isPlayerActive(@NonNull PlaybackStateCompat playbackStateCompat) {
|
||||
return playbackStateCompat.getState() == PlaybackStateCompat.STATE_BUFFERING ||
|
||||
playbackStateCompat.getState() == PlaybackStateCompat.STATE_PLAYING;
|
||||
}
|
||||
|
||||
private @NonNull MediaControllerCompat getMediaController() {
|
||||
return MediaControllerCompat.getMediaController(activity);
|
||||
}
|
||||
|
||||
|
||||
public void startConsecutivePlayback(@NonNull Uri audioSlideUri, long messageId, double progress) {
|
||||
startPlayback(audioSlideUri, messageId, progress, false);
|
||||
}
|
||||
|
||||
public void startSinglePlayback(@NonNull Uri audioSlideUri, long messageId, double progress) {
|
||||
startPlayback(audioSlideUri, messageId, progress, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the Media service to begin playback of a given audio slide. If the audio
|
||||
* slide is currently playing, we jump to the desired position and then begin playback.
|
||||
*
|
||||
* @param audioSlideUri The Uri of the desired audio slide
|
||||
* @param messageId The Message id of the given audio slide
|
||||
* @param progress The desired progress % to seek to.
|
||||
* @param singlePlayback The player will only play back the specified Uri, and not build a playlist.
|
||||
*/
|
||||
private void startPlayback(@NonNull Uri audioSlideUri, long messageId, double progress, boolean singlePlayback) {
|
||||
if (isCurrentTrack(audioSlideUri)) {
|
||||
long duration = getMediaController().getMetadata().getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||
|
||||
getMediaController().getTransportControls().seekTo((long) (duration * progress));
|
||||
getMediaController().getTransportControls().play();
|
||||
} else {
|
||||
Bundle extras = new Bundle();
|
||||
extras.putLong(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putDouble(EXTRA_PROGRESS, progress);
|
||||
extras.putBoolean(EXTRA_PLAY_SINGLE, singlePlayback);
|
||||
|
||||
getMediaController().getTransportControls().playFromUri(audioSlideUri, extras);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback if the given audio slide is playing.
|
||||
*
|
||||
* @param audioSlideUri The Uri of the audio slide to pause.
|
||||
*/
|
||||
public void pausePlayback(@NonNull Uri audioSlideUri) {
|
||||
if (isCurrentTrack(audioSlideUri)) {
|
||||
getMediaController().getTransportControls().pause();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to a given position if th given audio slide is playing. This call
|
||||
* is ignored if the given audio slide is not currently playing.
|
||||
*
|
||||
* @param audioSlideUri The Uri of the audio slide to seek.
|
||||
* @param progress The progress percentage to seek to.
|
||||
*/
|
||||
public void seekToPosition(@NonNull Uri audioSlideUri, double progress) {
|
||||
if (isCurrentTrack(audioSlideUri)) {
|
||||
long duration = getMediaController().getMetadata().getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||
|
||||
getMediaController().getTransportControls().pause();
|
||||
getMediaController().getTransportControls().seekTo((long) (duration * progress));
|
||||
getMediaController().getTransportControls().play();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops playback if the given audio slide is playing
|
||||
*
|
||||
* @param audioSlideUri The Uri of the audio slide to stop
|
||||
*/
|
||||
public void stopPlaybackAndReset(@NonNull Uri audioSlideUri) {
|
||||
if (isCurrentTrack(audioSlideUri)) {
|
||||
getMediaController().getTransportControls().stop();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCurrentTrack(@NonNull Uri uri) {
|
||||
MediaMetadataCompat metadataCompat = getMediaController().getMetadata();
|
||||
|
||||
return metadataCompat != null && Objects.equals(metadataCompat.getDescription().getMediaUri(), uri);
|
||||
}
|
||||
|
||||
private void notifyProgressEventHandler() {
|
||||
if (progressEventHandler == null) {
|
||||
progressEventHandler = new ProgressEventHandler(getMediaController(), voiceNotePlaybackState);
|
||||
progressEventHandler.sendEmptyMessage(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearProgressEventHandler() {
|
||||
if (progressEventHandler != null) {
|
||||
progressEventHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
|
||||
@Override
|
||||
public void onConnected() {
|
||||
try {
|
||||
MediaSessionCompat.Token token = mediaBrowser.getSessionToken();
|
||||
MediaControllerCompat mediaController = new MediaControllerCompat(activity, token);
|
||||
|
||||
MediaControllerCompat.setMediaController(activity, mediaController);
|
||||
|
||||
mediaController.registerCallback(mediaControllerCompatCallback);
|
||||
|
||||
mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState());
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "onConnected: Failed to set media controller", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ProgressEventHandler extends Handler {
|
||||
|
||||
private final MediaControllerCompat mediaController;
|
||||
private final MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState;
|
||||
|
||||
private ProgressEventHandler(@NonNull MediaControllerCompat mediaController,
|
||||
@NonNull MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState) {
|
||||
this.mediaController = mediaController;
|
||||
this.voiceNotePlaybackState = voiceNotePlaybackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
|
||||
if (isPlayerActive(mediaController.getPlaybackState()) &&
|
||||
mediaMetadataCompat != null &&
|
||||
mediaMetadataCompat.getDescription() != null)
|
||||
{
|
||||
|
||||
Uri mediaUri = Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri());
|
||||
boolean autoReset = Objects.equals(mediaUri, VoiceNotePlaybackPreparer.NEXT_URI) || Objects.equals(mediaUri, VoiceNotePlaybackPreparer.END_URI);
|
||||
VoiceNotePlaybackState previousState = voiceNotePlaybackState.getValue();
|
||||
long position = mediaController.getPlaybackState().getPosition();
|
||||
long duration = mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||
|
||||
if (previousState != null && Objects.equals(mediaUri, previousState.getUri())) {
|
||||
if (position < 0 && previousState.getPlayheadPositionMillis() >= 0) {
|
||||
position = previousState.getPlayheadPositionMillis();
|
||||
}
|
||||
|
||||
if (duration <= 0 && previousState.getTrackDuration() > 0) {
|
||||
duration = previousState.getTrackDuration();
|
||||
}
|
||||
}
|
||||
|
||||
if (duration > 0 && position >= 0 && position <= duration) {
|
||||
voiceNotePlaybackState.postValue(new VoiceNotePlaybackState(mediaUri, position, duration, autoReset));
|
||||
}
|
||||
|
||||
sendEmptyMessageDelayed(0, 50);
|
||||
} else {
|
||||
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class MediaControllerCompatCallback extends MediaControllerCompat.Callback {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(PlaybackStateCompat state) {
|
||||
if (isPlayerActive(state)) {
|
||||
notifyProgressEventHandler();
|
||||
} else {
|
||||
clearProgressEventHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Factory responsible for building out MediaDescriptionCompat objects for voice notes.
|
||||
*/
|
||||
class VoiceNoteMediaDescriptionCompatFactory {
|
||||
|
||||
public static final String EXTRA_MESSAGE_POSITION = "voice.note.extra.MESSAGE_POSITION";
|
||||
public static final String EXTRA_THREAD_RECIPIENT_ID = "voice.note.extra.RECIPIENT_ID";
|
||||
public static final String EXTRA_AVATAR_RECIPIENT_ID = "voice.note.extra.SENDER_ID";
|
||||
public static final String EXTRA_THREAD_ID = "voice.note.extra.THREAD_ID";
|
||||
public static final String EXTRA_COLOR = "voice.note.extra.COLOR";
|
||||
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
|
||||
|
||||
private VoiceNoteMediaDescriptionCompatFactory() {}
|
||||
|
||||
/**
|
||||
* Build out a MediaDescriptionCompat for a given voice note. Expects to be run
|
||||
* on a background thread.
|
||||
*
|
||||
* @param context Context.
|
||||
* @param messageRecord The MessageRecord of the given voice note.
|
||||
*
|
||||
* @return A MediaDescriptionCompat with all the details the service expects.
|
||||
*/
|
||||
@WorkerThread
|
||||
static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
|
||||
@NonNull MessageRecord messageRecord)
|
||||
{
|
||||
int startingPosition = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
.getMessagePositionInConversation(messageRecord.getThreadId(),
|
||||
messageRecord.getDateReceived());
|
||||
|
||||
Recipient threadRecipient = Objects.requireNonNull(DatabaseFactory.getThreadDatabase(context)
|
||||
.getRecipientForThreadId(messageRecord.getThreadId()));
|
||||
Recipient sender = messageRecord.isOutgoing() ? Recipient.self() : messageRecord.getIndividualRecipient();
|
||||
Recipient avatarRecipient = threadRecipient.isGroup() ? threadRecipient : sender;
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(EXTRA_THREAD_RECIPIENT_ID, threadRecipient.getId().serialize());
|
||||
extras.putString(EXTRA_AVATAR_RECIPIENT_ID, avatarRecipient.getId().serialize());
|
||||
extras.putLong(EXTRA_MESSAGE_POSITION, startingPosition);
|
||||
extras.putLong(EXTRA_THREAD_ID, messageRecord.getThreadId());
|
||||
extras.putString(EXTRA_COLOR, threadRecipient.getColor().serialize());
|
||||
extras.putLong(EXTRA_MESSAGE_ID, messageRecord.getId());
|
||||
|
||||
NotificationPrivacyPreference preference = TextSecurePreferences.getNotificationPrivacy(context);
|
||||
|
||||
String title;
|
||||
if (preference.isDisplayContact() && threadRecipient.isGroup()) {
|
||||
title = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__s_to_s,
|
||||
sender.getDisplayName(context),
|
||||
threadRecipient.getDisplayName(context));
|
||||
} else if (preference.isDisplayContact()) {
|
||||
title = sender.getDisplayName(context);
|
||||
} else {
|
||||
title = context.getString(R.string.MessageNotifier_signal_message);
|
||||
}
|
||||
|
||||
String subtitle = null;
|
||||
if (preference.isDisplayContact()) {
|
||||
subtitle = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__voice_message,
|
||||
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(),
|
||||
messageRecord.getDateReceived()));
|
||||
}
|
||||
|
||||
Uri uri = ((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri();
|
||||
|
||||
return new MediaDescriptionCompat.Builder()
|
||||
.setMediaUri(uri)
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.upstream.AssetDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
||||
|
||||
/**
|
||||
* This class is responsible for creating a MediaSource object for a given MediaDescriptionCompat
|
||||
*/
|
||||
final class VoiceNoteMediaSourceFactory {
|
||||
|
||||
private final Context context;
|
||||
|
||||
VoiceNoteMediaSourceFactory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MediaSource for a given MediaDescriptionCompat
|
||||
*
|
||||
* @param description The description to build from
|
||||
*
|
||||
* @return A preparable MediaSource
|
||||
*/
|
||||
public @Nullable MediaSource createMediaSource(MediaDescriptionCompat description) {
|
||||
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
||||
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
|
||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
|
||||
|
||||
return new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
|
||||
.setExtractorsFactory(extractorsFactory)
|
||||
.createMediaSource(description.getMediaUri());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
||||
public class VoiceNoteNotificationControlDispatcher extends DefaultControlDispatcher {
|
||||
|
||||
private final VoiceNoteQueueDataAdapter dataAdapter;
|
||||
|
||||
public VoiceNoteNotificationControlDispatcher(@NonNull VoiceNoteQueueDataAdapter dataAdapter) {
|
||||
this.dataAdapter = dataAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) {
|
||||
boolean isQueueToneIndex = windowIndex % 2 == 1;
|
||||
boolean isSeekingToStart = positionMs == C.TIME_UNSET;
|
||||
|
||||
if (isQueueToneIndex && isSeekingToStart) {
|
||||
int nextVoiceNoteWindowIndex = player.getCurrentWindowIndex() < windowIndex ? windowIndex + 1 : windowIndex - 1;
|
||||
|
||||
if (dataAdapter.size() <= nextVoiceNoteWindowIndex) {
|
||||
return super.dispatchSeekTo(player, windowIndex, positionMs);
|
||||
} else {
|
||||
return super.dispatchSeekTo(player, nextVoiceNoteWindowIndex, positionMs);
|
||||
}
|
||||
} else {
|
||||
return super.dispatchSeekTo(player, windowIndex, positionMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
class VoiceNoteNotificationManager {
|
||||
|
||||
private static final short NOW_PLAYING_NOTIFICATION_ID = 32221;
|
||||
|
||||
private final Context context;
|
||||
private final MediaControllerCompat controller;
|
||||
private final PlayerNotificationManager notificationManager;
|
||||
|
||||
VoiceNoteNotificationManager(@NonNull Context context,
|
||||
@NonNull MediaSessionCompat.Token token,
|
||||
@NonNull PlayerNotificationManager.NotificationListener listener,
|
||||
@NonNull VoiceNoteQueueDataAdapter dataAdapter)
|
||||
{
|
||||
this.context = context;
|
||||
|
||||
try {
|
||||
controller = new MediaControllerCompat(context, token);
|
||||
} catch (RemoteException e) {
|
||||
throw new IllegalArgumentException("Could not create a controller with given token");
|
||||
}
|
||||
|
||||
notificationManager = PlayerNotificationManager.createWithNotificationChannel(context,
|
||||
NotificationChannels.VOICE_NOTES,
|
||||
R.string.NotificationChannel_voice_notes,
|
||||
NOW_PLAYING_NOTIFICATION_ID,
|
||||
new DescriptionAdapter());
|
||||
|
||||
notificationManager.setMediaSessionToken(token);
|
||||
notificationManager.setSmallIcon(R.drawable.ic_notification);
|
||||
notificationManager.setRewindIncrementMs(0);
|
||||
notificationManager.setFastForwardIncrementMs(0);
|
||||
notificationManager.setNotificationListener(listener);
|
||||
notificationManager.setColorized(true);
|
||||
notificationManager.setControlDispatcher(new VoiceNoteNotificationControlDispatcher(dataAdapter));
|
||||
}
|
||||
|
||||
public void hideNotification() {
|
||||
notificationManager.setPlayer(null);
|
||||
}
|
||||
|
||||
public void showNotification(@NonNull Player player) {
|
||||
notificationManager.setPlayer(player);
|
||||
}
|
||||
|
||||
private final class DescriptionAdapter implements PlayerNotificationManager.MediaDescriptionAdapter {
|
||||
|
||||
private RecipientId cachedRecipientId;
|
||||
private Bitmap cachedBitmap;
|
||||
|
||||
@Override
|
||||
public String getCurrentContentTitle(Player player) {
|
||||
if (hasMetadata()) {
|
||||
return Objects.requireNonNull(controller.getMetadata().getDescription().getTitle()).toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PendingIntent createCurrentContentIntent(Player player) {
|
||||
if (!hasMetadata()) return null;
|
||||
|
||||
RecipientId recipientId = RecipientId.from(Objects.requireNonNull(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID)));
|
||||
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION);
|
||||
long threadId = controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID);
|
||||
|
||||
MaterialColor color;
|
||||
try {
|
||||
color = MaterialColor.fromSerialized(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_COLOR));
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
color = ContactColors.UNKNOWN_COLOR;
|
||||
}
|
||||
|
||||
notificationManager.setColor(color.toNotificationColor(context));
|
||||
|
||||
Intent conversationActivity = ConversationActivity.buildIntent(context,
|
||||
recipientId,
|
||||
threadId,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
startingPosition);
|
||||
|
||||
conversationActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
return PendingIntent.getActivity(context,
|
||||
0,
|
||||
conversationActivity,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentContentText(Player player) {
|
||||
if (hasMetadata()) {
|
||||
return Objects.toString(controller.getMetadata().getDescription().getSubtitle(), null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Bitmap getCurrentLargeIcon(Player player, PlayerNotificationManager.BitmapCallback callback) {
|
||||
if (!hasMetadata() || !TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact()) {
|
||||
cachedBitmap = null;
|
||||
cachedRecipientId = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
RecipientId currentRecipientId = RecipientId.from(Objects.requireNonNull(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_AVATAR_RECIPIENT_ID)));
|
||||
|
||||
if (Objects.equals(currentRecipientId, cachedRecipientId) && cachedBitmap != null) {
|
||||
return cachedBitmap;
|
||||
} else {
|
||||
cachedRecipientId = currentRecipientId;
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
cachedBitmap = AvatarUtil.getBitmapForNotification(context, Recipient.resolved(cachedRecipientId));
|
||||
callback.onBitmap(cachedBitmap);
|
||||
} catch (Exception e) {
|
||||
cachedBitmap = null;
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasMetadata() {
|
||||
return controller.getMetadata() != null && controller.getMetadata().getDescription() != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* ExoPlayer Preparer for Voice Notes. This only supports ACTION_PLAY_FROM_URI
|
||||
*/
|
||||
final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNotePlaybackPreparer.class);
|
||||
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
private static final long LIMIT = 5;
|
||||
|
||||
public static final Uri NEXT_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-down.ogg");
|
||||
public static final Uri END_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-up.ogg");
|
||||
|
||||
private final Context context;
|
||||
private final SimpleExoPlayer player;
|
||||
private final VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||
private final VoiceNoteMediaSourceFactory mediaSourceFactory;
|
||||
private final ConcatenatingMediaSource dataSource;
|
||||
|
||||
private boolean canLoadMore;
|
||||
private Uri latestUri = Uri.EMPTY;
|
||||
|
||||
VoiceNotePlaybackPreparer(@NonNull Context context,
|
||||
@NonNull SimpleExoPlayer player,
|
||||
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
|
||||
@NonNull VoiceNoteMediaSourceFactory mediaSourceFactory)
|
||||
{
|
||||
this.context = context;
|
||||
this.player = player;
|
||||
this.queueDataAdapter = queueDataAdapter;
|
||||
this.mediaSourceFactory = mediaSourceFactory;
|
||||
this.dataSource = new ConcatenatingMediaSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSupportedPrepareActions() {
|
||||
return PlaybackStateCompat.ACTION_PLAY_FROM_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepare() {
|
||||
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepare");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
||||
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromMediaId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromSearch(String query, Bundle extras) {
|
||||
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromSearch");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromUri(final Uri uri, Bundle extras) {
|
||||
Log.d(TAG, "onPrepareFromUri: " + uri);
|
||||
|
||||
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
|
||||
double progress = extras.getDouble(VoiceNoteMediaController.EXTRA_PROGRESS, 0);
|
||||
boolean singlePlayback = extras.getBoolean(VoiceNoteMediaController.EXTRA_PLAY_SINGLE, false);
|
||||
|
||||
canLoadMore = false;
|
||||
latestUri = uri;
|
||||
|
||||
queueDataAdapter.clear();
|
||||
dataSource.clear();
|
||||
|
||||
SimpleTask.run(EXECUTOR,
|
||||
() -> {
|
||||
if (singlePlayback) {
|
||||
return loadMediaDescriptionForSinglePlayback(messageId);
|
||||
} else {
|
||||
return loadMediaDescriptionsForConsecutivePlayback(messageId);
|
||||
}
|
||||
},
|
||||
descriptions -> {
|
||||
if (Util.hasItems(descriptions) && Objects.equals(latestUri, uri)) {
|
||||
applyDescriptionsToQueue(descriptions);
|
||||
|
||||
int window = Math.max(0, queueDataAdapter.indexOf(uri));
|
||||
|
||||
player.addListener(new Player.EventListener() {
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
|
||||
if (timeline.getWindowCount() >= window) {
|
||||
player.seekTo(window, (long) (player.getDuration() * progress));
|
||||
player.removeListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
player.prepare(dataSource);
|
||||
canLoadMore = !singlePlayback;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCommands() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
|
||||
}
|
||||
|
||||
private void applyDescriptionsToQueue(@NonNull List<MediaDescriptionCompat> descriptions) {
|
||||
for (MediaDescriptionCompat description : descriptions) {
|
||||
int holderIndex = queueDataAdapter.indexOf(description.getMediaUri());
|
||||
MediaDescriptionCompat next = createNextClone(description);
|
||||
int currentIndex = player.getCurrentWindowIndex();
|
||||
|
||||
if (holderIndex != -1) {
|
||||
queueDataAdapter.remove(holderIndex);
|
||||
|
||||
if (!queueDataAdapter.isEmpty()) {
|
||||
queueDataAdapter.remove(holderIndex);
|
||||
}
|
||||
|
||||
queueDataAdapter.add(holderIndex, createNextClone(description));
|
||||
queueDataAdapter.add(holderIndex, description);
|
||||
|
||||
if (currentIndex != holderIndex) {
|
||||
dataSource.removeMediaSource(holderIndex);
|
||||
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
|
||||
}
|
||||
|
||||
if (currentIndex != holderIndex + 1) {
|
||||
if (dataSource.getSize() > 1) {
|
||||
dataSource.removeMediaSource(holderIndex + 1);
|
||||
}
|
||||
|
||||
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
|
||||
}
|
||||
} else {
|
||||
int insertLocation = queueDataAdapter.indexAfter(description);
|
||||
|
||||
queueDataAdapter.add(insertLocation, next);
|
||||
queueDataAdapter.add(insertLocation, description);
|
||||
|
||||
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
|
||||
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
|
||||
}
|
||||
}
|
||||
|
||||
int lastIndex = queueDataAdapter.size() - 1;
|
||||
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
|
||||
|
||||
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
|
||||
queueDataAdapter.remove(lastIndex);
|
||||
dataSource.removeMediaSource(lastIndex);
|
||||
|
||||
if (queueDataAdapter.size() > 1) {
|
||||
MediaDescriptionCompat end = createEndClone(last);
|
||||
|
||||
queueDataAdapter.add(lastIndex, end);
|
||||
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull MediaDescriptionCompat createEndClone(@NonNull MediaDescriptionCompat source) {
|
||||
return buildUpon(source).setMediaId("end").setMediaUri(END_URI).build();
|
||||
}
|
||||
|
||||
private @NonNull MediaDescriptionCompat createNextClone(@NonNull MediaDescriptionCompat source) {
|
||||
return buildUpon(source).setMediaId("next").setMediaUri(NEXT_URI).build();
|
||||
}
|
||||
|
||||
private @NonNull MediaDescriptionCompat.Builder buildUpon(@NonNull MediaDescriptionCompat source) {
|
||||
return new MediaDescriptionCompat.Builder()
|
||||
.setSubtitle(source.getSubtitle())
|
||||
.setDescription(source.getDescription())
|
||||
.setTitle(source.getTitle())
|
||||
.setIconUri(source.getIconUri())
|
||||
.setIconBitmap(source.getIconBitmap())
|
||||
.setMediaId(source.getMediaId())
|
||||
.setExtras(source.getExtras());
|
||||
}
|
||||
|
||||
public void loadMoreVoiceNotes() {
|
||||
if (!canLoadMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
|
||||
long messageId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
|
||||
SimpleTask.run(EXECUTOR,
|
||||
() -> loadMediaDescriptionsForConsecutivePlayback(messageId),
|
||||
descriptions -> {
|
||||
if (Util.hasItems(descriptions) && canLoadMore) {
|
||||
applyDescriptionsToQueue(descriptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionForSinglePlayback(long messageId) {
|
||||
try {
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
|
||||
if (!MessageRecordUtil.hasAudio(messageRecord)) {
|
||||
Log.w(TAG, "Message does not contain audio.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.singletonList(VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context ,messageRecord));
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Could not find message.", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionsForConsecutivePlayback(long messageId) {
|
||||
try {
|
||||
List<MessageRecord> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context).getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
|
||||
|
||||
return Stream.of(buildFilteredMessageRecordList(recordsAfter))
|
||||
.map(record -> VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, record))
|
||||
.toList();
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Could not find message.", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull List<MessageRecord> buildFilteredMessageRecordList(@NonNull List<MessageRecord> recordsAfter) {
|
||||
return Stream.of(recordsAfter)
|
||||
.takeWhile(MessageRecordUtil::hasAudio)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.media.MediaBrowserServiceCompat;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Android Service responsible for playback of voice notes.
|
||||
*/
|
||||
public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNotePlaybackService.class);
|
||||
private static final String EMPTY_ROOT_ID = "empty-root-id";
|
||||
private static final int LOAD_MORE_THRESHOLD = 2;
|
||||
|
||||
private static final long SUPPORTED_ACTIONS = PlaybackStateCompat.ACTION_PLAY |
|
||||
PlaybackStateCompat.ACTION_PAUSE |
|
||||
PlaybackStateCompat.ACTION_SEEK_TO |
|
||||
PlaybackStateCompat.ACTION_STOP |
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE;
|
||||
|
||||
private MediaSessionCompat mediaSession;
|
||||
private MediaSessionConnector mediaSessionConnector;
|
||||
private PlaybackStateCompat.Builder stateBuilder;
|
||||
private SimpleExoPlayer player;
|
||||
private BecomingNoisyReceiver becomingNoisyReceiver;
|
||||
private VoiceNoteNotificationManager voiceNoteNotificationManager;
|
||||
private VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||
private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer;
|
||||
private VoiceNoteProximityManager voiceNoteProximityManager;
|
||||
private boolean isForegroundService;
|
||||
|
||||
private final LoadControl loadControl = new DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE)
|
||||
.createDefaultLoadControl();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
mediaSession = new MediaSessionCompat(this, TAG);
|
||||
stateBuilder = new PlaybackStateCompat.Builder()
|
||||
.setActions(SUPPORTED_ACTIONS);
|
||||
mediaSessionConnector = new MediaSessionConnector(mediaSession, null);
|
||||
becomingNoisyReceiver = new BecomingNoisyReceiver(this, mediaSession.getSessionToken());
|
||||
player = ExoPlayerFactory.newSimpleInstance(this, new DefaultRenderersFactory(this), new DefaultTrackSelector(), loadControl);
|
||||
queueDataAdapter = new VoiceNoteQueueDataAdapter();
|
||||
voiceNoteNotificationManager = new VoiceNoteNotificationManager(this,
|
||||
mediaSession.getSessionToken(),
|
||||
new VoiceNoteNotificationManagerListener(),
|
||||
queueDataAdapter);
|
||||
|
||||
VoiceNoteMediaSourceFactory mediaSourceFactory = new VoiceNoteMediaSourceFactory(this);
|
||||
|
||||
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory);
|
||||
voiceNoteProximityManager = new VoiceNoteProximityManager(this, player, queueDataAdapter);
|
||||
|
||||
mediaSession.setPlaybackState(stateBuilder.build());
|
||||
|
||||
player.addListener(new VoiceNotePlayerEventListener());
|
||||
player.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(C.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build(), true);
|
||||
|
||||
mediaSessionConnector.setPlayer(player, voiceNotePlaybackPreparer);
|
||||
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession, queueDataAdapter));
|
||||
|
||||
setSessionToken(mediaSession.getSessionToken());
|
||||
|
||||
mediaSession.setActive(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
|
||||
player.stop(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mediaSession.setActive(false);
|
||||
mediaSession.release();
|
||||
becomingNoisyReceiver.unregister();
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
|
||||
if (clientUid == Process.myUid()) {
|
||||
return new BrowserRoot(EMPTY_ROOT_ID, null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
|
||||
result.sendResult(Collections.emptyList());
|
||||
}
|
||||
|
||||
private class VoiceNotePlayerEventListener implements Player.EventListener {
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
switch (playbackState) {
|
||||
case Player.STATE_BUFFERING:
|
||||
case Player.STATE_READY:
|
||||
voiceNoteProximityManager.onPlayerReady();
|
||||
voiceNoteNotificationManager.showNotification(player);
|
||||
|
||||
if (!playWhenReady) {
|
||||
stopForeground(false);
|
||||
becomingNoisyReceiver.unregister();
|
||||
} else {
|
||||
becomingNoisyReceiver.register();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
voiceNoteProximityManager.onPlayerEnded();
|
||||
becomingNoisyReceiver.unregister();
|
||||
voiceNoteNotificationManager.hideNotification();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(int reason) {
|
||||
int currentWindowIndex = player.getCurrentWindowIndex();
|
||||
if (currentWindowIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
|
||||
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
|
||||
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + mediaDescriptionCompat.getMediaUri());
|
||||
}
|
||||
|
||||
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
|
||||
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();
|
||||
|
||||
if (isWithinThreshold && currentWindowIndex % 2 == 0) {
|
||||
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
Log.w(TAG, "ExoPlayer error occurred:", error);
|
||||
voiceNoteProximityManager.onPlayerError();
|
||||
}
|
||||
}
|
||||
|
||||
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
|
||||
|
||||
@Override
|
||||
public void onNotificationStarted(int notificationId, Notification notification) {
|
||||
if (!isForegroundService) {
|
||||
ContextCompat.startForegroundService(getApplicationContext(), new Intent(getApplicationContext(), VoiceNotePlaybackService.class));
|
||||
startForeground(notificationId, notification);
|
||||
isForegroundService = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationCancelled(int notificationId) {
|
||||
stopForeground(true);
|
||||
isForegroundService = false;
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiver to pause playback when things become noisy.
|
||||
*/
|
||||
private static class BecomingNoisyReceiver extends BroadcastReceiver {
|
||||
private static final IntentFilter NOISY_INTENT_FILTER = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
||||
|
||||
private final Context context;
|
||||
private final MediaControllerCompat controller;
|
||||
|
||||
private boolean registered;
|
||||
|
||||
private BecomingNoisyReceiver(Context context, MediaSessionCompat.Token token) {
|
||||
this.context = context;
|
||||
try {
|
||||
this.controller = new MediaControllerCompat(context, token);
|
||||
} catch (RemoteException e) {
|
||||
throw new IllegalArgumentException("Failed to create controller from token", e);
|
||||
}
|
||||
}
|
||||
|
||||
void register() {
|
||||
if (!registered) {
|
||||
context.registerReceiver(this, NOISY_INTENT_FILTER);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
if (registered) {
|
||||
context.unregisterReceiver(this);
|
||||
registered = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void onReceive(Context context, @NonNull Intent intent) {
|
||||
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
|
||||
controller.getTransportControls().pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Domain-level state object representing the state of the currently playing voice note.
|
||||
*/
|
||||
public class VoiceNotePlaybackState {
|
||||
|
||||
public static final VoiceNotePlaybackState NONE = new VoiceNotePlaybackState(Uri.EMPTY, 0, 0, false);
|
||||
|
||||
private final Uri uri;
|
||||
private final long playheadPositionMillis;
|
||||
private final long trackDuration;
|
||||
private final boolean autoReset;
|
||||
|
||||
public VoiceNotePlaybackState(@NonNull Uri uri, long playheadPositionMillis, long trackDuration, boolean autoReset) {
|
||||
this.uri = uri;
|
||||
this.playheadPositionMillis = playheadPositionMillis;
|
||||
this.trackDuration = trackDuration;
|
||||
this.autoReset = autoReset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Uri of the currently playing AudioSlide
|
||||
*/
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The last known playhead position
|
||||
*/
|
||||
public long getPlayheadPositionMillis() {
|
||||
return playheadPositionMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The track duration in ms
|
||||
*/
|
||||
public long getTrackDuration() {
|
||||
return trackDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if we should reset the currently playing clip.
|
||||
*/
|
||||
public boolean isAutoReset() {
|
||||
return autoReset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class VoiceNoteProximityManager implements SensorEventListener {
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteProximityManager.class);
|
||||
|
||||
private static final float PROXIMITY_THRESHOLD = 5f;
|
||||
|
||||
private final SimpleExoPlayer player;
|
||||
private final AudioManager audioManager;
|
||||
private final SensorManager sensorManager;
|
||||
private final Sensor proximitySensor;
|
||||
private final PowerManager.WakeLock wakeLock;
|
||||
private final VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||
|
||||
private long startTime;
|
||||
|
||||
VoiceNoteProximityManager(@NonNull Context context,
|
||||
@NonNull SimpleExoPlayer player,
|
||||
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter)
|
||||
{
|
||||
this.player = player;
|
||||
this.audioManager = ServiceUtil.getAudioManager(context);
|
||||
this.sensorManager = ServiceUtil.getSensorManager(context);
|
||||
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
|
||||
this.queueDataAdapter = queueDataAdapter;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
|
||||
} else {
|
||||
this.wakeLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
void onPlayerReady() {
|
||||
startTime = System.currentTimeMillis();
|
||||
sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
|
||||
}
|
||||
|
||||
void onPlayerEnded() {
|
||||
sensorManager.unregisterListener(this);
|
||||
|
||||
if (wakeLock != null && wakeLock.isHeld() && Build.VERSION.SDK_INT >= 21) {
|
||||
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
||||
}
|
||||
}
|
||||
|
||||
void onPlayerError() {
|
||||
onPlayerEnded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY || player.getPlaybackState() != Player.STATE_READY) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int desiredStreamType;
|
||||
if (event.values[0] < PROXIMITY_THRESHOLD && event.values[0] != proximitySensor.getMaximumRange()) {
|
||||
desiredStreamType = AudioManager.STREAM_VOICE_CALL;
|
||||
} else {
|
||||
desiredStreamType = AudioManager.STREAM_MUSIC;
|
||||
}
|
||||
|
||||
final int currentStreamType = Util.getStreamTypeForAudioUsage(player.getAudioAttributes().usage);
|
||||
|
||||
final long threadId;
|
||||
final int windowIndex = player.getCurrentWindowIndex();
|
||||
|
||||
if (queueDataAdapter.isEmpty() || windowIndex == C.INDEX_UNSET) {
|
||||
threadId = -1;
|
||||
} else {
|
||||
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(windowIndex);
|
||||
|
||||
threadId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID, -1);
|
||||
}
|
||||
|
||||
if (desiredStreamType == AudioManager.STREAM_VOICE_CALL &&
|
||||
desiredStreamType != currentStreamType &&
|
||||
!audioManager.isWiredHeadsetOn() &&
|
||||
threadId != -1 &&
|
||||
ApplicationDependencies.getMessageNotifier().getVisibleThread() == threadId)
|
||||
{
|
||||
if (wakeLock != null && !wakeLock.isHeld()) {
|
||||
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30));
|
||||
}
|
||||
|
||||
player.setPlayWhenReady(false);
|
||||
player.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(C.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(C.USAGE_VOICE_COMMUNICATION)
|
||||
.build());
|
||||
player.setPlayWhenReady(true);
|
||||
|
||||
startTime = System.currentTimeMillis();
|
||||
} else if (desiredStreamType == AudioManager.STREAM_MUSIC &&
|
||||
desiredStreamType != currentStreamType &&
|
||||
System.currentTimeMillis() - startTime > 500)
|
||||
{
|
||||
if (wakeLock != null) {
|
||||
if (wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
|
||||
player.setPlayWhenReady(false);
|
||||
player.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(C.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* DataAdapter which maintains the current queue of MediaDescriptionCompat objects.
|
||||
*/
|
||||
final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAdapter {
|
||||
|
||||
private final List<MediaDescriptionCompat> descriptions = new LinkedList<>();
|
||||
|
||||
@Override
|
||||
public MediaDescriptionCompat getMediaDescription(int position) {
|
||||
return descriptions.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int position, MediaDescriptionCompat description) {
|
||||
descriptions.add(position, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(int position) {
|
||||
descriptions.remove(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(int from, int to) {
|
||||
MediaDescriptionCompat description = descriptions.remove(from);
|
||||
descriptions.add(to, description);
|
||||
}
|
||||
|
||||
int size() {
|
||||
return descriptions.size();
|
||||
}
|
||||
|
||||
int indexOf(@NonNull Uri uri) {
|
||||
for (int i = 0; i < descriptions.size(); i++) {
|
||||
if (Objects.equals(uri, descriptions.get(i).getMediaUri())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int indexAfter(@NonNull MediaDescriptionCompat target) {
|
||||
if (isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long targetMessageId = target.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
for (int i = 0; i < descriptions.size(); i++) {
|
||||
long descriptionMessageId = descriptions.get(i).getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
|
||||
if (descriptionMessageId > targetMessageId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return descriptions.size();
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return descriptions.isEmpty();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
descriptions.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.thoughtcrime.securesms.components.voice;
|
||||
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator;
|
||||
|
||||
/**
|
||||
* Navigator to help support seek forward and back.
|
||||
*/
|
||||
final class VoiceNoteQueueNavigator extends TimelineQueueNavigator {
|
||||
|
||||
private final TimelineQueueEditor.QueueDataAdapter queueDataAdapter;
|
||||
|
||||
public VoiceNoteQueueNavigator(@NonNull MediaSessionCompat mediaSession, @NonNull TimelineQueueEditor.QueueDataAdapter queueDataAdapter) {
|
||||
super(mediaSession);
|
||||
this.queueDataAdapter = queueDataAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaDescriptionCompat getMediaDescription(Player player, int windowIndex) {
|
||||
return queueDataAdapter.getMediaDescription(windowIndex);
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ public final class CallParticipantsState {
|
||||
} else {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
|
||||
}
|
||||
} else if (callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE;
|
||||
}
|
||||
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
|
||||
@@ -58,6 +58,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
GestureDetectorCompat gestureDetector = new GestureDetectorCompat(child.getContext(), helper);
|
||||
|
||||
parent.setOnInterceptTouchEventListener((event) -> {
|
||||
final int action = event.getAction();
|
||||
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
|
||||
|
||||
if (pointerIndex > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (helper.velocityTracker == null) {
|
||||
helper.velocityTracker = VelocityTracker.obtain();
|
||||
}
|
||||
@@ -163,8 +170,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
activePointerId = e.getPointerId(0);
|
||||
lastTouchX = e.getX(activePointerId) + child.getX();
|
||||
lastTouchY = e.getY(activePointerId) + child.getY();
|
||||
lastTouchX = e.getX(0) + child.getX();
|
||||
lastTouchY = e.getY(0) + child.getY();
|
||||
isDragging = true;
|
||||
pipWidth = child.getMeasuredWidth();
|
||||
pipHeight = child.getMeasuredHeight();
|
||||
@@ -175,7 +182,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
int pointerIndex = e2.findPointerIndex(activePointerId);
|
||||
int pointerIndex = e2.findPointerIndex(activePointerId);
|
||||
|
||||
if (pointerIndex == -1) {
|
||||
fling();
|
||||
return false;
|
||||
}
|
||||
|
||||
float x = e2.getX(pointerIndex) + child.getX();
|
||||
float y = e2.getY(pointerIndex) + child.getY();
|
||||
float dx = x - lastTouchX;
|
||||
|
||||
@@ -22,7 +22,6 @@ import androidx.constraintlayout.widget.Guideline;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.AutoTransition;
|
||||
import androidx.transition.ChangeBounds;
|
||||
import androidx.transition.Transition;
|
||||
import androidx.transition.TransitionManager;
|
||||
import androidx.transition.TransitionSet;
|
||||
@@ -455,7 +454,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
if (!visibleViewSet.equals(lastVisibleSet) || !controls.isFadeOutEnabled()) {
|
||||
fadeInNewUiState(lastVisibleSet, webRtcControls.displaySmallOngoingCallButtons());
|
||||
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop()));
|
||||
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +509,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
private void fadeInControls() {
|
||||
fadeControls(ConstraintSet.VISIBLE);
|
||||
pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop());
|
||||
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
|
||||
|
||||
scheduleFadeOut();
|
||||
}
|
||||
@@ -553,6 +552,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
Transition transition = new AutoTransition().setOrdering(TransitionSet.ORDERING_TOGETHER)
|
||||
.setDuration(TRANSITION_DURATION_MILLIS);
|
||||
|
||||
TransitionManager.endTransitions(parent);
|
||||
TransitionManager.beginDelayedTransition(parent, transition);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
@@ -570,6 +570,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {
|
||||
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
|
||||
|
||||
TransitionManager.endTransitions(parent);
|
||||
TransitionManager.beginDelayedTransition(parent, transition);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
@@ -64,6 +65,10 @@ public class ContactRepository {
|
||||
String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE));
|
||||
String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL));
|
||||
|
||||
if (phone != null) {
|
||||
phone = PhoneNumberFormatter.prettyPrint(phone);
|
||||
}
|
||||
|
||||
return Util.getFirstNonEmpty(phone, email);
|
||||
}));
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.thoughtcrime.securesms.contacts.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Fallback resource based contact photo with a 20dp icon
|
||||
*/
|
||||
public final class FallbackPhoto20dp implements FallbackContactPhoto {
|
||||
|
||||
@DrawableRes private final int drawable20dp;
|
||||
|
||||
public FallbackPhoto20dp(@DrawableRes int drawable20dp) {
|
||||
this.drawable20dp = drawable20dp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color) {
|
||||
return buildDrawable(context, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
||||
return buildDrawable(context, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
|
||||
return buildDrawable(context, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asCallCard(Context context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context, int color) {
|
||||
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
|
||||
Drawable foreground = AppCompatResources.getDrawable(context, drawable20dp);
|
||||
Drawable gradient = ThemeUtil.getThemedDrawable(context, R.attr.resource_placeholder_gradient);
|
||||
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
|
||||
int foregroundInset = ViewUtil.dpToPx(2);
|
||||
|
||||
DrawableCompat.setTint(background, color);
|
||||
|
||||
drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset);
|
||||
|
||||
return drawable;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
|
||||
return new LayerDrawable(new Drawable[] { base, gradient });
|
||||
}
|
||||
|
||||
return new ResourceContactPhoto(fallbackResId).asDrawable(context, color, inverted);
|
||||
return newFallbackDrawable(context, color, inverted);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,6 +66,14 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
|
||||
return asDrawable(context, color, inverted);
|
||||
}
|
||||
|
||||
protected @DrawableRes int getFallbackResId() {
|
||||
return fallbackResId;
|
||||
}
|
||||
|
||||
protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return new ResourceContactPhoto(fallbackResId).asDrawable(context, color, inverted);
|
||||
}
|
||||
|
||||
private @Nullable String getAbbreviation(String name) {
|
||||
String[] parts = name.split(" ");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
@@ -7,13 +7,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
@@ -67,7 +64,7 @@ public class ProfileContactPhoto implements ContactPhoto {
|
||||
}
|
||||
|
||||
private long getFileLastModified() {
|
||||
if (!recipient.isLocalNumber()) {
|
||||
if (!recipient.isSelf()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@@ -38,6 +41,8 @@ class ContactDiscoveryV2 {
|
||||
|
||||
private static final String TAG = Log.tag(ContactDiscoveryV2.class);
|
||||
|
||||
private static final int MAX_NUMBERS = 20_500;
|
||||
|
||||
@WorkerThread
|
||||
static DirectoryResult getDirectoryResult(@NonNull Context context,
|
||||
@NonNull Set<String> databaseNumbers,
|
||||
@@ -47,7 +52,14 @@ class ContactDiscoveryV2 {
|
||||
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
|
||||
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
|
||||
Set<String> sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers());
|
||||
Set<String> ignoredNumbers = new HashSet<>();
|
||||
|
||||
if (sanitizedNumbers.size() > MAX_NUMBERS) {
|
||||
Set<String> randomlySelected = randomlySelect(sanitizedNumbers, MAX_NUMBERS);
|
||||
|
||||
ignoredNumbers = SetUtil.difference(sanitizedNumbers, randomlySelected);
|
||||
sanitizedNumbers = randomlySelected;
|
||||
}
|
||||
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
KeyStore iasKeyStore = getIasKeyStore(context);
|
||||
@@ -56,7 +68,7 @@ class ContactDiscoveryV2 {
|
||||
Map<String, UUID> results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE);
|
||||
FuzzyPhoneNumberHelper.OutputResultV2 outputResult = FuzzyPhoneNumberHelper.generateOutputV2(results, inputResult);
|
||||
|
||||
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites());
|
||||
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers);
|
||||
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException e) {
|
||||
Log.w(TAG, "Attestation error.", e);
|
||||
throw new IOException(e);
|
||||
@@ -77,6 +89,13 @@ class ContactDiscoveryV2 {
|
||||
}).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static @NonNull Set<String> randomlySelect(@NonNull Set<String> numbers, int max) {
|
||||
List<String> list = new ArrayList<>(numbers);
|
||||
Collections.shuffle(list);
|
||||
|
||||
return new HashSet<>(list.subList(0, max));
|
||||
}
|
||||
|
||||
private static KeyStore getIasKeyStore(@NonNull Context context) {
|
||||
try {
|
||||
TrustStore contactTrustStore = new IasTrustStore(context);
|
||||
|
||||
@@ -29,8 +29,6 @@ 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;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
@@ -45,7 +43,6 @@ 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;
|
||||
@@ -86,7 +83,7 @@ public class DirectoryHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
|
||||
if (!Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
|
||||
Log.w(TAG, "No contact permissions. Skipping.");
|
||||
return;
|
||||
}
|
||||
@@ -238,6 +235,7 @@ public class DirectoryHelper {
|
||||
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
|
||||
.filterNot(activeNumbers::contains)
|
||||
.filterNot(n -> result.getNumberRewrites().containsKey(n))
|
||||
.filterNot(n -> result.getIgnoredNumbers().contains(n))
|
||||
.map(recipientDatabase::getOrInsertFromE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
@@ -398,7 +396,7 @@ public class DirectoryHelper {
|
||||
|
||||
for (RecipientId newUser: newUsers) {
|
||||
Recipient recipient = Recipient.resolved(newUser);
|
||||
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isLocalNumber()) {
|
||||
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isSelf()) {
|
||||
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
|
||||
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
|
||||
|
||||
@@ -471,12 +469,15 @@ public class DirectoryHelper {
|
||||
static class DirectoryResult {
|
||||
private final Map<String, UUID> registeredNumbers;
|
||||
private final Map<String, String> numberRewrites;
|
||||
private final Set<String> ignoredNumbers;
|
||||
|
||||
DirectoryResult(@NonNull Map<String, UUID> registeredNumbers,
|
||||
@NonNull Map<String, String> numberRewrites)
|
||||
@NonNull Map<String, String> numberRewrites,
|
||||
@NonNull Set<String> ignoredNumbers)
|
||||
{
|
||||
this.registeredNumbers = registeredNumbers;
|
||||
this.numberRewrites = numberRewrites;
|
||||
this.ignoredNumbers = ignoredNumbers;
|
||||
}
|
||||
|
||||
|
||||
@@ -487,6 +488,10 @@ public class DirectoryHelper {
|
||||
@NonNull Map<String, String> getNumberRewrites() {
|
||||
return numberRewrites;
|
||||
}
|
||||
|
||||
@NonNull Set<String> getIgnoredNumbers() {
|
||||
return ignoredNumbers;
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnlistedResult {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -10,7 +8,6 @@ import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -19,6 +16,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -84,7 +82,7 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
||||
}
|
||||
|
||||
public void onMediaChanged(@NonNull List<Media> media) {
|
||||
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
if (StorageUtil.canReadFromMediaStore()) {
|
||||
mediaAdapter.setMedia(media);
|
||||
permissionButton.setVisibility(GONE);
|
||||
permissionText.setVisibility(GONE);
|
||||
|
||||
@@ -39,10 +39,10 @@ import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
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.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
@@ -69,8 +69,10 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
@@ -98,7 +100,6 @@ import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
@@ -116,6 +117,7 @@ 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.PendingGroupJoinRequestsReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||
@@ -163,10 +165,12 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeResult;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
|
||||
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.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
@@ -206,6 +210,9 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
|
||||
@@ -242,8 +249,11 @@ 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.SmsUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
@@ -340,6 +350,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
protected Stub<ReminderView> reminderView;
|
||||
private Stub<UnverifiedBannerView> unverifiedBannerView;
|
||||
private Stub<GroupShareProfileView> groupShareProfileView;
|
||||
private Stub<ReviewBannerView> reviewBanner;
|
||||
private TypingStatusTextWatcher typingTextWatcher;
|
||||
private ConversationSearchBottomBar searchNav;
|
||||
private MenuItem searchViewItem;
|
||||
@@ -366,6 +377,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private ConversationViewModel viewModel;
|
||||
private InviteReminderModel inviteReminderModel;
|
||||
private ConversationGroupViewModel groupViewModel;
|
||||
private MentionsPickerViewModel mentionsViewModel;
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
@@ -442,12 +454,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
initializeStickerObserver();
|
||||
initializeViewModel();
|
||||
initializeGroupViewModel();
|
||||
if (FeatureFlags.mentions()) initializeMentionsViewModel();
|
||||
initializeMentionsViewModel();
|
||||
initializeEnabledCheck();
|
||||
initializePendingRequestsBanner();
|
||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
initializeProfiles();
|
||||
initializeGv1Migration();
|
||||
initializeDraft().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean loadedDraft) {
|
||||
@@ -556,7 +570,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
fragment.setLastSeen(System.currentTimeMillis());
|
||||
markLastSeen();
|
||||
AudioSlidePlayer.stopAll();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@@ -815,10 +828,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
} else {
|
||||
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
|
||||
}
|
||||
inflater.inflate(R.menu.conversation_active_group_options, menu);
|
||||
} else if (isActiveV2Group || isActiveGroup) {
|
||||
inflater.inflate(R.menu.conversation_active_group_options, menu);
|
||||
}
|
||||
|
||||
inflater.inflate(R.menu.conversation_active_group_options, menu);
|
||||
}
|
||||
|
||||
inflater.inflate(R.menu.conversation, menu);
|
||||
@@ -836,7 +848,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
inflater.inflate(R.menu.conversation_add_to_contacts, menu);
|
||||
}
|
||||
|
||||
if (recipient != null && recipient.get().isLocalNumber()) {
|
||||
if (recipient != null && recipient.get().isSelf()) {
|
||||
if (isSecureText) {
|
||||
hideMenuItem(menu, R.id.menu_call_secure);
|
||||
hideMenuItem(menu, R.id.menu_video_secure);
|
||||
@@ -865,7 +877,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
if (isActiveV2Group) {
|
||||
hideMenuItem(menu, R.id.menu_mute_notifications);
|
||||
hideMenuItem(menu, R.id.menu_conversation_settings);
|
||||
} else if (isActiveGroup) {
|
||||
} else if (isGroupConversation()) {
|
||||
hideMenuItem(menu, R.id.menu_conversation_settings);
|
||||
}
|
||||
|
||||
@@ -978,9 +990,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
Log.d(TAG, "onBackPressed()");
|
||||
if (reactionOverlay.isShowing()) reactionOverlay.hide();
|
||||
else if (container.isInputOpen()) container.hideCurrentInput(composeText);
|
||||
else super.onBackPressed();
|
||||
if (reactionOverlay.isShowing()) {
|
||||
reactionOverlay.hide();
|
||||
} else if (container.isInputOpen()) {
|
||||
container.hideCurrentInput(composeText);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1033,6 +1049,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.onAllGranted(() -> viewModel.onAttachmentKeyboardOpen())
|
||||
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -1123,9 +1140,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
private void handleMakeDefaultSms() {
|
||||
Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT);
|
||||
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getPackageName());
|
||||
startActivityForResult(intent, SMS_DEFAULT);
|
||||
startActivityForResult(SmsUtil.getSmsRoleIntent(this), SMS_DEFAULT);
|
||||
}
|
||||
|
||||
private void handleRegisterForSignal() {
|
||||
@@ -1221,7 +1236,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
@NonNull Recipient recipient)
|
||||
{
|
||||
IconCompat icon = IconCompat.createWithAdaptiveBitmap(bitmap);
|
||||
String name = recipient.isLocalNumber() ? context.getString(R.string.note_to_self)
|
||||
String name = recipient.isSelf() ? context.getString(R.string.note_to_self)
|
||||
: recipient.getDisplayName(context);
|
||||
|
||||
ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, recipient.getId().serialize() + '-' + System.currentTimeMillis())
|
||||
@@ -1527,6 +1542,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
});
|
||||
}
|
||||
|
||||
private void initializePendingRequestsBanner() {
|
||||
groupViewModel.getActionableRequestingMembers()
|
||||
.observe(this, actionablePendingGroupRequests -> updateReminders());
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
@@ -1680,7 +1700,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
protected void updateReminders() {
|
||||
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
|
||||
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
|
||||
Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue();
|
||||
|
||||
if (UnauthorizedReminder.isEligible(this)) {
|
||||
reminderView.get().showReminder(new UnauthorizedReminder(this));
|
||||
@@ -1698,6 +1719,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reminderView.get().setOnActionClickListener(this::handleReminderAction);
|
||||
reminderView.get().setOnDismissListener(() -> inviteReminderModel.dismissReminder());
|
||||
reminderView.get().showReminder(inviteReminder.get());
|
||||
} else if (actionableRequestingMembers != null && actionableRequestingMembers > 0 && FeatureFlags.groupsV2manageGroupLinks()) {
|
||||
reminderView.get().showReminder(PendingGroupJoinRequestsReminder.create(this, actionableRequestingMembers));
|
||||
reminderView.get().setOnActionClickListener(id -> {
|
||||
if (id == R.id.reminder_action_review_join_requests) {
|
||||
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(this, getRecipient().getGroupId().get().requireV2()));
|
||||
}
|
||||
});
|
||||
} else if (reminderView.resolved()) {
|
||||
reminderView.get().hide();
|
||||
}
|
||||
@@ -1810,6 +1838,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
|
||||
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
|
||||
groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub);
|
||||
reviewBanner = ViewUtil.findStubById(this, R.id.review_banner_stub);
|
||||
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
|
||||
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
|
||||
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
|
||||
@@ -1978,10 +2007,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
groupViewModel = ViewModelProviders.of(this, new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
|
||||
recipient.observe(this, groupViewModel::onRecipientChange);
|
||||
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
|
||||
groupViewModel.getReviewState().observe(this, this::presentGroupReviewBanner);
|
||||
}
|
||||
|
||||
private void initializeMentionsViewModel() {
|
||||
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||
mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||
|
||||
recipient.observe(this, r -> {
|
||||
if (r.isPushV2Group() && !mentionsSuggestions.resolved()) {
|
||||
@@ -2106,6 +2136,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
RetrieveProfileJob.enqueueAsync(recipient.getId());
|
||||
}
|
||||
|
||||
private void initializeGv1Migration() {
|
||||
GroupV1MigrationJob.enqueuePossibleAutoMigrate(recipient.getId());
|
||||
}
|
||||
|
||||
private void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
Log.i(TAG, "onModified(" + recipient.getId() + ") " + recipient.getRegistered());
|
||||
titleView.setTitle(glideRequests, recipient);
|
||||
@@ -2123,6 +2157,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
if (groupViewModel != null) {
|
||||
groupViewModel.onRecipientChange(recipient);
|
||||
}
|
||||
|
||||
if (mentionsViewModel != null) {
|
||||
mentionsViewModel.onRecipientChange(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@@ -2227,6 +2265,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private Drafts getDraftsForCurrentState() {
|
||||
Drafts drafts = new Drafts();
|
||||
|
||||
if (recipient.get().isGroup() && !recipient.get().isActiveGroup()) {
|
||||
return drafts;
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(composeText)) {
|
||||
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed().toString()));
|
||||
List<Mention> draftMentions = composeText.getMentions();
|
||||
@@ -2377,7 +2419,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
if (!TextSecurePreferences.isPushRegistered(this)) return false;
|
||||
if (recipient.get().isGroup()) return false;
|
||||
|
||||
return recipient.get().isLocalNumber();
|
||||
return recipient.get().isSelf();
|
||||
}
|
||||
|
||||
private boolean isGroupConversation() {
|
||||
@@ -3036,7 +3078,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
|
||||
messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
|
||||
|
||||
viewModel.getRecipient().observe(this, this::presentMessageRequestBottomViewTo);
|
||||
viewModel.getRequestReviewDisplayState().observe(this, this::presentRequestReviewBanner);
|
||||
viewModel.getMessageData().observe(this, this::presentMessageRequestBottomViewTo);
|
||||
viewModel.getMessageRequestDisplayState().observe(this, this::presentMessageRequestDisplayState);
|
||||
viewModel.getFailures().observe(this, this::showGroupChangeErrorToast);
|
||||
viewModel.getMessageRequestStatus().observe(this, status -> {
|
||||
@@ -3061,6 +3104,42 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
});
|
||||
}
|
||||
|
||||
private void presentRequestReviewBanner(@NonNull MessageRequestViewModel.RequestReviewDisplayState state) {
|
||||
switch (state) {
|
||||
case SHOWN:
|
||||
reviewBanner.get().setVisibility(View.VISIBLE);
|
||||
|
||||
CharSequence message = new SpannableStringBuilder().append(SpanUtil.bold(getString(R.string.ConversationFragment__review_requests_carefully)))
|
||||
.append(" ")
|
||||
.append(getString(R.string.ConversationFragment__signal_found_another_contact_with_the_same_name));
|
||||
|
||||
reviewBanner.get().setBannerMessage(message);
|
||||
|
||||
Drawable drawable = Objects.requireNonNull(ThemeUtil.getThemedDrawable(this, R.attr.menu_info_icon)).mutate();
|
||||
DrawableCompat.setTint(drawable, ThemeUtil.getThemedColor(this, R.attr.icon_tint));
|
||||
|
||||
reviewBanner.get().setBannerIcon(drawable);
|
||||
reviewBanner.get().setOnClickListener(unused -> handleReviewRequest(recipient.getId()));
|
||||
break;
|
||||
case HIDDEN:
|
||||
reviewBanner.get().setVisibility(View.GONE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void presentGroupReviewBanner(@NonNull ConversationGroupViewModel.ReviewState groupReviewState) {
|
||||
if (groupReviewState.getCount() > 0) {
|
||||
reviewBanner.get().setVisibility(View.VISIBLE);
|
||||
reviewBanner.get().setBannerMessage(getString(R.string.ConversationFragment__d_group_members_have_the_same_name, groupReviewState.getCount()));
|
||||
reviewBanner.get().setBannerRecipient(groupReviewState.getRecipient());
|
||||
reviewBanner.get().setOnClickListener(unused -> handleReviewGroupMembers(groupReviewState.getGroupId()));
|
||||
} else if (reviewBanner.resolved()) {
|
||||
reviewBanner.get().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void showMessageRequestBusy() {
|
||||
messageRequestBottomView.showBusy();
|
||||
}
|
||||
@@ -3069,6 +3148,24 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
messageRequestBottomView.hideBusy();
|
||||
}
|
||||
|
||||
private void handleReviewGroupMembers(@Nullable GroupId.V2 groupId) {
|
||||
if (groupId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReviewCardDialogFragment.createForReviewMembers(groupId)
|
||||
.show(getSupportFragmentManager(), null);
|
||||
}
|
||||
|
||||
private void handleReviewRequest(@NonNull RecipientId recipientId) {
|
||||
if (recipientId == Recipient.UNKNOWN.getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReviewCardDialogFragment.createForReviewRequest(recipientId)
|
||||
.show(getSupportFragmentManager(), null);
|
||||
}
|
||||
|
||||
private void showGroupChangeErrorToast(@NonNull GroupChangeFailureReason e) {
|
||||
Toast.makeText(this, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
@@ -3081,7 +3178,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
{
|
||||
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionOverlay.setOnHideListener(onHideListener);
|
||||
reactionOverlay.show(this, maskTarget, messageRecord, inputAreaHeight());
|
||||
reactionOverlay.show(this, maskTarget, recipient.get(), messageRecord, inputAreaHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3269,7 +3366,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private void presentMessageRequestDisplayState(@NonNull MessageRequestViewModel.DisplayState displayState) {
|
||||
if (getIntent().hasExtra(TEXT_EXTRA) || getIntent().hasExtra(MEDIA_EXTRA) || getIntent().hasExtra(STICKER_EXTRA)) {
|
||||
if ((getIntent().hasExtra(TEXT_EXTRA) && !Util.isEmpty(getIntent().getStringExtra(TEXT_EXTRA))) ||
|
||||
getIntent().hasExtra(MEDIA_EXTRA) ||
|
||||
getIntent().hasExtra(STICKER_EXTRA))
|
||||
{
|
||||
Log.d(TAG, "[presentMessageRequestDisplayState] Have extra, so ignoring provided state.");
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
} else if (isPushGroupV1Conversation() && !isActiveGroup()) {
|
||||
@@ -3284,7 +3384,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
groupShareProfileView.get().setVisibility(View.GONE);
|
||||
}
|
||||
break;
|
||||
case DISPLAY_LEGACY:
|
||||
case DISPLAY_PRE_MESSAGE_REQUEST:
|
||||
if (recipient.get().isGroup()) {
|
||||
groupShareProfileView.get().setRecipient(recipient.get());
|
||||
groupShareProfileView.get().setVisibility(View.VISIBLE);
|
||||
@@ -3449,10 +3549,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void presentMessageRequestBottomViewTo(@Nullable Recipient recipient) {
|
||||
if (recipient == null) return;
|
||||
private void presentMessageRequestBottomViewTo(@Nullable MessageRequestViewModel.MessageData messageData) {
|
||||
if (messageData == null) return;
|
||||
|
||||
messageRequestBottomView.setRecipient(recipient);
|
||||
messageRequestBottomView.setMessageData(messageData);
|
||||
}
|
||||
|
||||
private static class KeyboardImageDetails {
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.Manifest;
|
||||
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;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
@@ -52,6 +52,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -70,6 +71,8 @@ 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;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
|
||||
@@ -86,6 +89,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -99,6 +103,8 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
@@ -115,12 +121,12 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -179,6 +185,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
private Animation mentionButtonOutAnimation;
|
||||
private OnScrollListener conversationScrollListener;
|
||||
private int pulsePosition = -1;
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
|
||||
public static void prepare(@NonNull Context context) {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
@@ -226,7 +233,10 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
new ConversationItemSwipeCallback(
|
||||
conversationMessage -> actionMode == null &&
|
||||
MenuState.canReplyToMessage(MenuState.isActionMessage(conversationMessage.getMessageRecord()), conversationMessage.getMessageRecord(), messageRequestViewModel.shouldShowMessageRequest()),
|
||||
MenuState.canReplyToMessage(recipient.get(),
|
||||
MenuState.isActionMessage(conversationMessage.getMessageRecord()),
|
||||
conversationMessage.getMessageRecord(),
|
||||
messageRequestViewModel.shouldShowMessageRequest()),
|
||||
this::handleReplyMessage
|
||||
).attachToRecyclerView(list);
|
||||
|
||||
@@ -303,6 +313,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
initializeResources();
|
||||
initializeMessageRequestViewModel();
|
||||
initializeListAdapter();
|
||||
voiceNoteMediaController = new VoiceNoteMediaController((AppCompatActivity) requireActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -345,9 +356,17 @@ public class ConversationFragment extends LoggingFragment {
|
||||
actionMode.finish();
|
||||
}
|
||||
|
||||
long oldThreadId = threadId;
|
||||
|
||||
initializeResources();
|
||||
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
|
||||
initializeListAdapter();
|
||||
|
||||
int startingPosition = getStartPosition();
|
||||
if (startingPosition != -1 && oldThreadId == threadId) {
|
||||
list.post(() -> moveToPosition(startingPosition, () -> Log.w(TAG, "Could not scroll to requested message.")));
|
||||
} else {
|
||||
initializeListAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
public void moveToLastSeen() {
|
||||
@@ -365,6 +384,10 @@ public class ConversationFragment extends LoggingFragment {
|
||||
snapToTopDataObserver.requestScrollPosition(position);
|
||||
}
|
||||
|
||||
private int getStartPosition() {
|
||||
return requireActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
|
||||
}
|
||||
|
||||
private void initializeMessageRequestViewModel() {
|
||||
MessageRequestViewModel.Factory factory = new MessageRequestViewModel.Factory(requireContext());
|
||||
|
||||
@@ -411,7 +434,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
} else if (isSelf) {
|
||||
conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation));
|
||||
} else {
|
||||
String subtitle = recipient.getE164().orNull();
|
||||
String subtitle = recipient.getE164().transform(PhoneNumberFormatter::prettyPrint).orNull();
|
||||
|
||||
if (subtitle == null || subtitle.equals(title)) {
|
||||
conversationBanner.hideSubtitle();
|
||||
@@ -452,7 +475,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
private void initializeResources() {
|
||||
long oldThreadId = threadId;
|
||||
|
||||
int startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
|
||||
int startingPosition = getStartPosition();
|
||||
|
||||
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
|
||||
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
|
||||
@@ -573,7 +596,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
return;
|
||||
}
|
||||
|
||||
MenuState menuState = MenuState.getMenuState(Stream.of(messages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()), messageRequestViewModel.shouldShowMessageRequest());
|
||||
MenuState menuState = MenuState.getMenuState(recipient.get(), 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());
|
||||
@@ -662,53 +685,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
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 {
|
||||
buildLegacyDeleteConfirmationDialog(messageRecords).show();
|
||||
}
|
||||
}
|
||||
|
||||
private AlertDialog.Builder buildLegacyDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
|
||||
int messagesCount = messageRecords.size();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messagesCount, messagesCount));
|
||||
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
|
||||
builder.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
|
||||
R.string.ConversationFragment_deleting,
|
||||
R.string.ConversationFragment_deleting_messages)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
boolean threadDeleted;
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
return builder;
|
||||
buildRemoteDeleteConfirmationDialog(messageRecords).show();
|
||||
}
|
||||
|
||||
private AlertDialog.Builder buildRemoteDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
|
||||
@@ -769,7 +746,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
deleteForEveryone.run();
|
||||
} else {
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
.setMessage(R.string.ConversationFragment_this_message_will_be_permanently_deleted_for_everyone)
|
||||
.setMessage(R.string.ConversationFragment_this_message_will_be_deleted_for_everyone_in_the_conversation)
|
||||
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> {
|
||||
SignalStore.uiHints().markHasConfirmedDeleteForEveryoneOnce();
|
||||
deleteForEveryone.run();
|
||||
@@ -881,26 +858,40 @@ public class ConversationFragment extends LoggingFragment {
|
||||
throw new AssertionError("Cannot save a view-once message.");
|
||||
}
|
||||
|
||||
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
|
||||
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
|
||||
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
|
||||
.toList();
|
||||
if (!Util.isEmpty(attachments)) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "No slide with attachable media found, failing nicely.");
|
||||
Toast.makeText(getActivity(),
|
||||
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
|
||||
Toast.LENGTH_LONG).show();
|
||||
SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> {
|
||||
if (StorageUtil.canWriteToMediaStore()) {
|
||||
performSave(message);
|
||||
return;
|
||||
}
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> performSave(message))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
private void performSave(final MediaMmsMessageRecord message) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
|
||||
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
|
||||
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
|
||||
.toList();
|
||||
|
||||
if (!Util.isEmpty(attachments)) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "No slide with attachable media found, failing nicely.");
|
||||
Toast.makeText(getActivity(),
|
||||
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void clearHeaderIfNotTyping(ConversationAdapter adapter) {
|
||||
if (adapter.getHeaderView() != typingView) {
|
||||
adapter.setHeaderView(null);
|
||||
@@ -1223,11 +1214,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
MessageRecord messageRecord = conversationMessage.getMessageRecord();
|
||||
|
||||
if (messageRecord.isSecure() &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
!messageRecord.isUpdate() &&
|
||||
!recipient.get().isBlocked() &&
|
||||
!messageRequestViewModel.shouldShowMessageRequest() &&
|
||||
if (messageRecord.isSecure() &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
!messageRecord.isUpdate() &&
|
||||
!recipient.get().isBlocked() &&
|
||||
!messageRequestViewModel.shouldShowMessageRequest() &&
|
||||
(!recipient.get().isGroup() || recipient.get().isActiveGroup()) &&
|
||||
((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
|
||||
{
|
||||
isReacting = true;
|
||||
@@ -1397,10 +1389,40 @@ public class ConversationFragment extends LoggingFragment {
|
||||
listener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePause(@NonNull Uri uri) {
|
||||
voiceNoteMediaController.pausePlayback(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
|
||||
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
|
||||
voiceNoteMediaController.seekToPosition(uri, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), onPlaybackStartObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUrlClicked(@NonNull String url) {
|
||||
return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients) {
|
||||
GroupsV1MigrationBottomSheetDialogFragment.showForLearnMore(requireFragmentManager(), pendingRecipients);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,40 +10,73 @@ import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
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.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ConversationGroupViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Recipient> liveRecipient;
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
|
||||
private final LiveData<Integer> actionableRequestingMembers;
|
||||
private final LiveData<ReviewState> reviewState;
|
||||
|
||||
private ConversationGroupViewModel() {
|
||||
this.liveRecipient = new MutableLiveData<>();
|
||||
|
||||
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
|
||||
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
|
||||
LiveData<List<Recipient>> duplicates = LiveDataUtil.mapAsync(groupRecord, record -> {
|
||||
if (record != null && record.isV2Group()) {
|
||||
return Stream.of(ReviewUtil.getDuplicatedRecipients(record.getId().requireV2()))
|
||||
.map(ReviewRecipient::getRecipient)
|
||||
.toList();
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
});
|
||||
|
||||
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
|
||||
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
|
||||
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
|
||||
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
|
||||
duplicates,
|
||||
(record, dups) -> dups.isEmpty()
|
||||
? ReviewState.EMPTY
|
||||
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
|
||||
|
||||
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
|
||||
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
|
||||
}
|
||||
|
||||
void onRecipientChange(Recipient recipient) {
|
||||
liveRecipient.setValue(recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of pending group join requests that can be actioned by this client.
|
||||
*/
|
||||
LiveData<Integer> getActionableRequestingMembers() {
|
||||
return actionableRequestingMembers;
|
||||
}
|
||||
|
||||
LiveData<GroupActiveState> getGroupActiveState() {
|
||||
return groupActiveState;
|
||||
}
|
||||
@@ -52,6 +85,10 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
return selfMembershipLevel;
|
||||
}
|
||||
|
||||
public LiveData<ReviewState> getReviewState() {
|
||||
return reviewState;
|
||||
}
|
||||
|
||||
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient != null && recipient.isGroup()) {
|
||||
Application context = ApplicationDependencies.getApplication();
|
||||
@@ -62,6 +99,20 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private static int mapToActionableRequestingMemberCount(@Nullable GroupRecord record) {
|
||||
if (record != null &&
|
||||
FeatureFlags.groupsV2manageGroupLinks() &&
|
||||
record.isV2Group() &&
|
||||
record.memberLevel(Recipient.self()) == GroupDatabase.MemberLevel.ADMINISTRATOR)
|
||||
{
|
||||
return record.requireV2GroupProperties()
|
||||
.getDecryptedGroup()
|
||||
.getRequestingMembersCount();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
@@ -93,6 +144,33 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
static final class ReviewState {
|
||||
|
||||
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
private final Recipient recipient;
|
||||
private final int count;
|
||||
|
||||
ReviewState(@Nullable GroupId.V2 groupId, @NonNull Recipient recipient, int count) {
|
||||
this.groupId = groupId;
|
||||
this.recipient = recipient;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public @Nullable GroupId.V2 getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
static final class GroupActiveState {
|
||||
private final boolean isActive;
|
||||
private final boolean isActiveV2;
|
||||
|
||||
@@ -93,6 +93,7 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
@@ -112,6 +113,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
@@ -127,6 +129,7 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
|
||||
@@ -420,14 +423,17 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
|
||||
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
|
||||
bodyBubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_reveal_viewed_background_color), PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
|
||||
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
|
||||
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
|
||||
} else {
|
||||
bodyBubble.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
|
||||
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
}
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_sent_text_secondary_color));
|
||||
@@ -620,6 +626,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
bodyText.setText(italics);
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
bodyText.setOverflowText(null);
|
||||
} else if (isCaptionlessMms(messageRecord)) {
|
||||
bodyText.setVisibility(View.GONE);
|
||||
} else {
|
||||
@@ -652,7 +659,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
{
|
||||
boolean showControls = !messageRecord.isFailed();
|
||||
|
||||
if (isViewOnceMessage(messageRecord)) {
|
||||
if (eventListener != null && audioViewStub.resolved()) {
|
||||
Log.d(TAG, "setMediaAttributes: unregistering voice note callbacks for audio slide " + audioViewStub.get().getAudioSlideUri());
|
||||
eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
|
||||
}
|
||||
|
||||
if (isViewOnceMessage(messageRecord) && !messageRecord.isRemoteDelete()) {
|
||||
revealableStub.get().setVisibility(VISIBLE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
@@ -734,11 +746,17 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
|
||||
audioViewStub.get().setAudio(Objects.requireNonNull(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide()), new AudioViewCallbacks(), showControls, false);
|
||||
audioViewStub.get().setDownloadClickListener(singleDownloadClickListener);
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
if (eventListener != null) {
|
||||
Log.d(TAG, "setMediaAttributes: registered listener for audio slide " + audioViewStub.get().getAudioSlideUri());
|
||||
eventListener.onRegisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
|
||||
} else {
|
||||
Log.w(TAG, "setMediaAttributes: could not register listener for audio slide " + audioViewStub.get().getAudioSlideUri());
|
||||
}
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
@@ -1521,7 +1539,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (eventListener != null) {
|
||||
if (eventListener != null && batchSelected.isEmpty()) {
|
||||
VibrateUtil.vibrateTick(context);
|
||||
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
|
||||
}
|
||||
@@ -1531,6 +1549,40 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
public void updateDrawState(@NonNull TextPaint ds) { }
|
||||
}
|
||||
|
||||
private final class AudioViewCallbacks implements AudioView.Callbacks {
|
||||
|
||||
@Override
|
||||
public void onPlay(@NonNull Uri audioUri, double progress) {
|
||||
if (eventListener == null) return;
|
||||
|
||||
eventListener.onVoiceNotePlay(audioUri, messageRecord.getId(), progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause(@NonNull Uri audioUri) {
|
||||
if (eventListener == null) return;
|
||||
|
||||
eventListener.onVoiceNotePause(audioUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekTo(@NonNull Uri audioUri, double progress) {
|
||||
if (eventListener == null) return;
|
||||
|
||||
eventListener.onVoiceNoteSeekTo(audioUri, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopAndReset(@NonNull Uri audioUri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressUpdated(long durationMillis, long playheadMillis) {
|
||||
footer.setAudioDuration(durationMillis, playheadMillis);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessageApproval() {
|
||||
final int title;
|
||||
final int message;
|
||||
|
||||
@@ -5,9 +5,7 @@ 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;
|
||||
@@ -24,7 +22,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@@ -60,6 +57,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
private final PointF lastSeenDownPoint = new PointF();
|
||||
|
||||
private Activity activity;
|
||||
private Recipient conversationRecipient;
|
||||
private MessageRecord messageRecord;
|
||||
private OverlayState overlayState = OverlayState.HIDDEN;
|
||||
|
||||
@@ -145,15 +143,21 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
maskView.setTargetParentTranslationY(translationY);
|
||||
}
|
||||
|
||||
public void show(@NonNull Activity activity, @NonNull View maskTarget, @NonNull MessageRecord messageRecord, int maskPaddingBottom) {
|
||||
public void show(@NonNull Activity activity,
|
||||
@NonNull View maskTarget,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
int maskPaddingBottom)
|
||||
{
|
||||
|
||||
if (overlayState != OverlayState.HIDDEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageRecord = messageRecord;
|
||||
overlayState = OverlayState.UNINITAILIZED;
|
||||
selected = -1;
|
||||
this.messageRecord = messageRecord;
|
||||
this.conversationRecipient = conversationRecipient;
|
||||
overlayState = OverlayState.UNINITAILIZED;
|
||||
selected = -1;
|
||||
|
||||
setupToolbarMenuItems();
|
||||
setupSelectedEmoji();
|
||||
@@ -498,7 +502,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
}
|
||||
|
||||
private void setupToolbarMenuItems() {
|
||||
MenuState menuState = MenuState.getMenuState(Collections.singleton(messageRecord), false);
|
||||
MenuState menuState = MenuState.getMenuState(conversationRecipient, Collections.singleton(messageRecord), false);
|
||||
|
||||
toolbar.getMenu().findItem(R.id.action_copy).setVisible(menuState.shouldShowCopyAction());
|
||||
toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction());
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -97,7 +96,7 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
startDrawable = R.drawable.ic_volume_off_white_18dp;
|
||||
}
|
||||
|
||||
if (recipient != null && recipient.isSystemContact() && !recipient.isLocalNumber()) {
|
||||
if (recipient != null && recipient.isSystemContact() && !recipient.isSelf()) {
|
||||
endDrawable = R.drawable.ic_profile_circle_outline_16;
|
||||
}
|
||||
|
||||
@@ -125,7 +124,7 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
|
||||
private void setRecipientTitle(Recipient recipient) {
|
||||
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
|
||||
else if (recipient.isLocalNumber()) setSelfTitle();
|
||||
else if (recipient.isSelf()) setSelfTitle();
|
||||
else setIndividualRecipientTitle(recipient);
|
||||
}
|
||||
|
||||
@@ -145,8 +144,8 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
private void setGroupRecipientTitle(Recipient recipient) {
|
||||
this.title.setText(recipient.getDisplayName(getContext()));
|
||||
this.subtitle.setText(Stream.of(recipient.getParticipants())
|
||||
.sorted((a, b) -> Boolean.compare(a.isLocalNumber(), b.isLocalNumber()))
|
||||
.map(r -> r.isLocalNumber() ? getResources().getString(R.string.ConversationTitleView_you)
|
||||
.sorted((a, b) -> Boolean.compare(a.isSelf(), b.isSelf()))
|
||||
.map(r -> r.isSelf() ? getResources().getString(R.string.ConversationTitleView_you)
|
||||
: r.getDisplayName(getContext()))
|
||||
.collect(Collectors.joining(", ")));
|
||||
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -30,10 +25,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@@ -50,15 +42,14 @@ public final class ConversationUpdateItem extends LinearLayout
|
||||
|
||||
private Set<ConversationMessage> batchSelected;
|
||||
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
private TextView body;
|
||||
private TextView date;
|
||||
private LiveRecipient sender;
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private LiveData<SpannableString> displayBody;
|
||||
private TextView body;
|
||||
private TextView actionButton;
|
||||
private LiveRecipient sender;
|
||||
private ConversationMessage conversationMessage;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
private MessageRecord messageRecord;
|
||||
private LiveData<Spannable> displayBody;
|
||||
private EventListener eventListener;
|
||||
|
||||
private final UpdateObserver updateObserver = new UpdateObserver();
|
||||
private final SenderObserver senderObserver = new SenderObserver();
|
||||
@@ -74,11 +65,8 @@ public final class ConversationUpdateItem extends LinearLayout
|
||||
@Override
|
||||
public void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
this.icon = findViewById(R.id.conversation_update_icon);
|
||||
this.title = findViewById(R.id.conversation_update_title);
|
||||
this.body = findViewById(R.id.conversation_update_body);
|
||||
this.date = findViewById(R.id.conversation_update_date);
|
||||
this.body = findViewById(R.id.conversation_update_body);
|
||||
this.actionButton = findViewById(R.id.conversation_update_action);
|
||||
|
||||
this.setOnClickListener(new InternalClickListener(null));
|
||||
}
|
||||
@@ -97,12 +85,12 @@ public final class ConversationUpdateItem extends LinearLayout
|
||||
{
|
||||
this.batchSelected = batchSelected;
|
||||
|
||||
bind(lifecycleOwner, conversationMessage, locale);
|
||||
bind(lifecycleOwner, conversationMessage, nextMessageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEventListener(@Nullable EventListener listener) {
|
||||
// No events to report yet
|
||||
this.eventListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,29 +98,28 @@ public final class ConversationUpdateItem extends LinearLayout
|
||||
return conversationMessage;
|
||||
}
|
||||
|
||||
private void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
|
||||
private void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord)
|
||||
{
|
||||
this.conversationMessage = conversationMessage;
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.locale = locale;
|
||||
this.nextMessageRecord = nextMessageRecord;
|
||||
|
||||
observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
|
||||
|
||||
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
|
||||
LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
|
||||
LiveData<SpannableString> spannableStringMessage = toSpannable(loading(liveUpdateMessage));
|
||||
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
|
||||
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription);
|
||||
LiveData<Spannable> spannableMessage = loading(liveUpdateMessage);
|
||||
|
||||
present(conversationMessage);
|
||||
present(conversationMessage, nextMessageRecord);
|
||||
|
||||
observeDisplayBody(lifecycleOwner, spannableStringMessage);
|
||||
observeDisplayBody(lifecycleOwner, spannableMessage);
|
||||
}
|
||||
|
||||
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
|
||||
private @NonNull LiveData<String> loading(@NonNull LiveData<String> string) {
|
||||
return LiveDataUtil.until(string, LiveDataUtil.delay(250, getContext().getString(R.string.ConversationUpdateItem_loading)));
|
||||
}
|
||||
|
||||
private static LiveData<SpannableString> toSpannable(LiveData<String> loading) {
|
||||
return Transformations.map(loading, source -> source == null ? null : new SpannableString(source));
|
||||
private @NonNull LiveData<Spannable> loading(@NonNull LiveData<Spannable> string) {
|
||||
return LiveDataUtil.until(string, LiveDataUtil.delay(250, new SpannableString(getContext().getString(R.string.ConversationUpdateItem_loading))));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -152,7 +139,7 @@ public final class ConversationUpdateItem extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<SpannableString> displayBody) {
|
||||
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<Spannable> displayBody) {
|
||||
if (this.displayBody != displayBody) {
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.removeObserver(updateObserver);
|
||||
@@ -175,97 +162,24 @@ public final class ConversationUpdateItem extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void present(ConversationMessage conversationMessage) {
|
||||
MessageRecord messageRecord = conversationMessage.getMessageRecord();
|
||||
if (messageRecord.isGroupAction()) setGroupRecord();
|
||||
else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
|
||||
else if (messageRecord.isJoined()) setJoinedRecord();
|
||||
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
|
||||
else if (messageRecord.isEndSession()) setEndSessionRecord();
|
||||
else if (messageRecord.isIdentityUpdate()) setIdentityRecord();
|
||||
else if (messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord);
|
||||
else if (messageRecord.isProfileChange()) setProfileNameChangeRecord();
|
||||
else throw new AssertionError("Neither group nor log nor joined.");
|
||||
|
||||
private void present(ConversationMessage conversationMessage, @NonNull Optional<MessageRecord> nextMessageRecord) {
|
||||
if (batchSelected.contains(conversationMessage)) setSelected(true);
|
||||
else setSelected(false);
|
||||
}
|
||||
|
||||
private void setCallRecord(MessageRecord messageRecord) {
|
||||
if (messageRecord.isIncomingCall()) icon.setImageResource(R.drawable.ic_call_received_grey600_24dp);
|
||||
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
|
||||
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
|
||||
|
||||
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateSent()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
date.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void setTimerRecord(final MessageRecord messageRecord) {
|
||||
if (messageRecord.getExpiresIn() > 0) {
|
||||
icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_timer_24));
|
||||
if (conversationMessage.getMessageRecord().isGroupV1MigrationEvent() &&
|
||||
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isGroupV1MigrationEvent()))
|
||||
{
|
||||
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationEventInvites());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_timer_disabled_24));
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
}
|
||||
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
title.setText(ExpirationUtil.getExpirationDisplayValue(getContext(), (int)(messageRecord.getExpiresIn() / 1000)));
|
||||
|
||||
title.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private ColorFilter getIconTintFilter() {
|
||||
return new PorterDuffColorFilter(ThemeUtil.getThemedColor(getContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
private void setIdentityRecord() {
|
||||
icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.safety_number_icon));
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
|
||||
title.setVisibility(GONE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setIdentityVerifyUpdate(final MessageRecord messageRecord) {
|
||||
if (messageRecord.isIdentityVerified()) icon.setImageResource(R.drawable.ic_check_white_24dp);
|
||||
else icon.setImageResource(R.drawable.ic_info_outline_white_24);
|
||||
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
|
||||
title.setVisibility(GONE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setProfileNameChangeRecord() {
|
||||
icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_outline_20));
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
|
||||
title.setVisibility(GONE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setGroupRecord() {
|
||||
icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.menu_group_icon));
|
||||
icon.clearColorFilter();
|
||||
|
||||
title.setVisibility(GONE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setJoinedRecord() {
|
||||
icon.setImageResource(R.drawable.ic_favorite_grey600_24dp);
|
||||
icon.clearColorFilter();
|
||||
|
||||
title.setVisibility(GONE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setEndSessionRecord() {
|
||||
icon.setImageResource(R.drawable.ic_refresh_white_24dp);
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -277,14 +191,14 @@ public final class ConversationUpdateItem extends LinearLayout
|
||||
|
||||
@Override
|
||||
public void onChanged(Recipient recipient) {
|
||||
present(conversationMessage);
|
||||
present(conversationMessage, nextMessageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private final class UpdateObserver implements Observer<SpannableString> {
|
||||
private final class UpdateObserver implements Observer<Spannable> {
|
||||
|
||||
@Override
|
||||
public void onChanged(SpannableString update) {
|
||||
public void onChanged(Spannable update) {
|
||||
setBodyText(update);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@@ -50,7 +51,8 @@ final class MenuState {
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MenuState getMenuState(@NonNull Set<MessageRecord> messageRecords,
|
||||
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
|
||||
@NonNull Set<MessageRecord> messageRecords,
|
||||
boolean shouldShowMessageRequest)
|
||||
{
|
||||
|
||||
@@ -102,20 +104,21 @@ final class MenuState {
|
||||
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null)
|
||||
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce && !remoteDelete)
|
||||
.shouldShowDetailsAction(!actionMessage)
|
||||
.shouldShowReplyAction(canReplyToMessage(actionMessage, messageRecord, shouldShowMessageRequest));
|
||||
.shouldShowReplyAction(canReplyToMessage(conversationRecipient, actionMessage, messageRecord, shouldShowMessageRequest));
|
||||
}
|
||||
|
||||
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
|
||||
.build();
|
||||
}
|
||||
|
||||
static boolean canReplyToMessage(boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) {
|
||||
return !actionMessage &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
!messageRecord.isPending() &&
|
||||
!messageRecord.isFailed() &&
|
||||
!isDisplayingMessageRequest &&
|
||||
messageRecord.isSecure() &&
|
||||
static boolean canReplyToMessage(@NonNull Recipient conversationRecipient, boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) {
|
||||
return !actionMessage &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
!messageRecord.isPending() &&
|
||||
!messageRecord.isFailed() &&
|
||||
!isDisplayingMessageRequest &&
|
||||
messageRecord.isSecure() &&
|
||||
(!conversationRecipient.isGroup() || conversationRecipient.isActiveGroup()) &&
|
||||
!messageRecord.getRecipient().isBlocked();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -24,13 +25,14 @@ import java.util.List;
|
||||
|
||||
public class MentionsPickerFragment extends LoggingFragment {
|
||||
|
||||
private MentionsPickerAdapter adapter;
|
||||
private RecyclerView list;
|
||||
private View topDivider;
|
||||
private View bottomDivider;
|
||||
private BottomSheetBehavior<View> behavior;
|
||||
private MentionsPickerViewModel viewModel;
|
||||
private Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
|
||||
private MentionsPickerAdapter adapter;
|
||||
private RecyclerView list;
|
||||
private View topDivider;
|
||||
private View bottomDivider;
|
||||
private BottomSheetBehavior<View> behavior;
|
||||
private MentionsPickerViewModel viewModel;
|
||||
private final Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
@@ -112,10 +114,10 @@ public class MentionsPickerFragment extends LoggingFragment {
|
||||
if (isShowing) {
|
||||
list.scrollToPosition(0);
|
||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
list.post(lockSheetAfterListUpdate);
|
||||
handler.post(lockSheetAfterListUpdate);
|
||||
showDividers(true);
|
||||
} else {
|
||||
list.getHandler().removeCallbacks(lockSheetAfterListUpdate);
|
||||
handler.removeCallbacks(lockSheetAfterListUpdate);
|
||||
behavior.setHideable(true);
|
||||
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ final class MentionsPickerRepository {
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<Recipient> search(@NonNull MentionQuery mentionQuery) {
|
||||
if (mentionQuery.query == null) {
|
||||
if (mentionQuery.query == null || mentionQuery.members.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
@@ -54,14 +54,12 @@ import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -149,6 +147,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
MegaphoneActionController
|
||||
{
|
||||
public static final short MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME = 32562;
|
||||
public static final short SMS_ROLE_REQUEST_CODE = 32563;
|
||||
|
||||
private static final String TAG = Log.tag(ConversationListFragment.class);
|
||||
|
||||
@@ -590,7 +589,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
} else if (OutdatedBuildReminder.isEligible()) {
|
||||
return Optional.of(new OutdatedBuildReminder(context));
|
||||
} else if (DefaultSmsReminder.isEligible(context)) {
|
||||
return Optional.of(new DefaultSmsReminder(context));
|
||||
return Optional.of(new DefaultSmsReminder(this, SMS_ROLE_REQUEST_CODE));
|
||||
} else if (Util.isDefaultSmsProvider(context) && SystemSmsImportReminder.isEligible(context)) {
|
||||
return Optional.of((new SystemSmsImportReminder(context)));
|
||||
} else if (PushRegistrationReminder.isEligible(context)) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import android.view.View;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
@@ -171,7 +172,7 @@ public final class ConversationListItem extends RelativeLayout
|
||||
|
||||
this.recipient.observeForever(this);
|
||||
if (highlightSubstring != null) {
|
||||
String name = recipient.get().isLocalNumber() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
|
||||
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
|
||||
|
||||
this.fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), name, highlightSubstring));
|
||||
} else {
|
||||
@@ -365,7 +366,11 @@ public final class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
private void setStatusIcons(ThreadRecord thread) {
|
||||
if (!thread.isOutgoing() || thread.isOutgoingCall() || thread.isVerificationStatusChange()) {
|
||||
if (!thread.isOutgoing() ||
|
||||
thread.isOutgoingAudioCall() ||
|
||||
thread.isOutgoingVideoCall() ||
|
||||
thread.isVerificationStatusChange())
|
||||
{
|
||||
deliveryStatusIndicator.setNone();
|
||||
alertView.setNone();
|
||||
} else if (thread.isFailed()) {
|
||||
@@ -377,10 +382,23 @@ public final class ConversationListItem extends RelativeLayout
|
||||
} else {
|
||||
alertView.setNone();
|
||||
|
||||
if (thread.isPending()) deliveryStatusIndicator.setPending();
|
||||
else if (thread.isRemoteRead()) deliveryStatusIndicator.setRead();
|
||||
else if (thread.isDelivered()) deliveryStatusIndicator.setDelivered();
|
||||
else deliveryStatusIndicator.setSent();
|
||||
if (thread.getExtra() != null && thread.getExtra().isRemoteDelete()) {
|
||||
if (thread.isPending()) {
|
||||
deliveryStatusIndicator.setPending();
|
||||
} else {
|
||||
deliveryStatusIndicator.setNone();
|
||||
}
|
||||
} else {
|
||||
if (thread.isPending()) {
|
||||
deliveryStatusIndicator.setPending();
|
||||
} else if (thread.isRemoteRead()) {
|
||||
deliveryStatusIndicator.setRead();
|
||||
} else if (thread.isDelivered()) {
|
||||
deliveryStatusIndicator.setDelivered();
|
||||
} else {
|
||||
deliveryStatusIndicator.setSent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,71 +427,68 @@ public final class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
return emphasisAdded(recipientToStringAsync(thread.getGroupAddedBy(),
|
||||
r -> context.getString(thread.isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
|
||||
: R.string.ThreadRecord_s_added_you_to_the_group,
|
||||
r.getDisplayName(context))));
|
||||
} else if (!thread.isMessageRequestAccepted()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_message_request));
|
||||
if (!thread.isMessageRequestAccepted()) {
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_request));
|
||||
} else if (SmsDatabase.Types.isGroupUpdate(thread.getType())) {
|
||||
if (thread.getRecipient().isPushV2Group()) {
|
||||
return emphasisAdded(MessageRecord.getGv2ChangeDescription(context, thread.getBody()));
|
||||
return emphasisAdded(context, MessageRecord.getGv2ChangeDescription(context, thread.getBody()));
|
||||
} else {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_group_updated));
|
||||
}
|
||||
} else if (SmsDatabase.Types.isGroupQuit(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_left_the_group));
|
||||
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message));
|
||||
return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message));
|
||||
} else if (SmsDatabase.Types.isFailedDecryptType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
||||
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
||||
} else if (SmsDatabase.Types.isNoRemoteSessionType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
|
||||
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
|
||||
} else if (SmsDatabase.Types.isEndSessionType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_secure_session_reset));
|
||||
} else if (MmsSmsColumns.Types.isLegacyType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
return emphasisAdded(context, context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
} else if (MmsSmsColumns.Types.isDraftMessageType(thread.getType())) {
|
||||
String draftText = context.getString(R.string.ThreadRecord_draft);
|
||||
return emphasisAdded(draftText + " " + thread.getBody());
|
||||
} else if (SmsDatabase.Types.isOutgoingCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_called));
|
||||
} else if (SmsDatabase.Types.isIncomingCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_called_you));
|
||||
} else if (SmsDatabase.Types.isMissedCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_missed_call));
|
||||
return emphasisAdded(context, draftText + " " + thread.getBody());
|
||||
} else if (SmsDatabase.Types.isOutgoingAudioCall(thread.getType()) || SmsDatabase.Types.isOutgoingVideoCall(thread.getType())) {
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_called));
|
||||
} else if (SmsDatabase.Types.isIncomingAudioCall(thread.getType()) || SmsDatabase.Types.isIncomingVideoCall(thread.getType())) {
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_called_you));
|
||||
} else if (SmsDatabase.Types.isMissedAudioCall(thread.getType())) {
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_audio_call));
|
||||
} else if (SmsDatabase.Types.isMissedVideoCall(thread.getType())) {
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_video_call));
|
||||
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context))));
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> new SpannableString(context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context)))));
|
||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
|
||||
int seconds = (int)(thread.getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
|
||||
} else if (SmsDatabase.Types.isIdentityUpdate(thread.getType())) {
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> {
|
||||
if (r.isGroup()) {
|
||||
return context.getString(R.string.ThreadRecord_safety_number_changed);
|
||||
return new SpannableString(context.getString(R.string.ThreadRecord_safety_number_changed));
|
||||
} else {
|
||||
return context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context));
|
||||
return new SpannableString(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)));
|
||||
}
|
||||
}));
|
||||
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
} else if (SmsDatabase.Types.isIdentityDefault(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_unverified));
|
||||
} else if (SmsDatabase.Types.isUnsupportedMessageType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_message_could_not_be_processed));
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed));
|
||||
} else if (SmsDatabase.Types.isProfileChange(thread.getType())) {
|
||||
return emphasisAdded("");
|
||||
return emphasisAdded(context, "");
|
||||
} else {
|
||||
ThreadDatabase.Extra extra = thread.getExtra();
|
||||
if (extra != null && extra.isViewOnce()) {
|
||||
return emphasisAdded(getViewOnceDescription(context, thread.getContentType()));
|
||||
return emphasisAdded(context, getViewOnceDescription(context, thread.getContentType()));
|
||||
} else if (extra != null && extra.isRemoteDelete()) {
|
||||
return emphasisAdded(context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted));
|
||||
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted));
|
||||
} else {
|
||||
return LiveDataUtil.just(new SpannableString(removeNewlines(thread.getBody())));
|
||||
}
|
||||
@@ -492,15 +507,15 @@ public final class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull String string) {
|
||||
return emphasisAdded(UpdateDescription.staticDescription(string));
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull String string) {
|
||||
return emphasisAdded(context, UpdateDescription.staticDescription(string, 0, 0));
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull UpdateDescription description) {
|
||||
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(description));
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull UpdateDescription description) {
|
||||
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(context, description));
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<String> description) {
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<Spannable> description) {
|
||||
return Transformations.map(description, sequence -> {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(Typeface.ITALIC),
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
@@ -62,6 +63,9 @@ public final class GroupDatabase extends Database {
|
||||
private static final String TIMESTAMP = "timestamp";
|
||||
static final String ACTIVE = "active";
|
||||
static final String MMS = "mms";
|
||||
private static final String EXPECTED_V2_ID = "expected_v2_id";
|
||||
private static final String FORMER_V1_MEMBERS = "former_v1_members";
|
||||
|
||||
|
||||
/* V2 Group columns */
|
||||
/** 32 bytes serialized {@link GroupMasterKey} */
|
||||
@@ -71,32 +75,33 @@ public final class GroupDatabase extends Database {
|
||||
/** Serialized {@link DecryptedGroup} protobuf */
|
||||
private static final String V2_DECRYPTED_GROUP = "decrypted_group";
|
||||
|
||||
public static final String CREATE_TABLE =
|
||||
"CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
GROUP_ID + " TEXT, " +
|
||||
RECIPIENT_ID + " INTEGER, " +
|
||||
TITLE + " TEXT, " +
|
||||
MEMBERS + " TEXT, " +
|
||||
AVATAR_ID + " INTEGER, " +
|
||||
AVATAR_KEY + " BLOB, " +
|
||||
AVATAR_CONTENT_TYPE + " TEXT, " +
|
||||
AVATAR_RELAY + " TEXT, " +
|
||||
TIMESTAMP + " INTEGER, " +
|
||||
ACTIVE + " INTEGER DEFAULT 1, " +
|
||||
AVATAR_DIGEST + " BLOB, " +
|
||||
MMS + " INTEGER DEFAULT 0, " +
|
||||
V2_MASTER_KEY + " BLOB, " +
|
||||
V2_REVISION + " BLOB, " +
|
||||
V2_DECRYPTED_GROUP + " BLOB);";
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
GROUP_ID + " TEXT, " +
|
||||
RECIPIENT_ID + " INTEGER, " +
|
||||
TITLE + " TEXT, " +
|
||||
MEMBERS + " TEXT, " +
|
||||
AVATAR_ID + " INTEGER, " +
|
||||
AVATAR_KEY + " BLOB, " +
|
||||
AVATAR_CONTENT_TYPE + " TEXT, " +
|
||||
AVATAR_RELAY + " TEXT, " +
|
||||
TIMESTAMP + " INTEGER, " +
|
||||
ACTIVE + " INTEGER DEFAULT 1, " +
|
||||
AVATAR_DIGEST + " BLOB, " +
|
||||
MMS + " INTEGER DEFAULT 0, " +
|
||||
V2_MASTER_KEY + " BLOB, " +
|
||||
V2_REVISION + " BLOB, " +
|
||||
V2_DECRYPTED_GROUP + " BLOB, " +
|
||||
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
|
||||
FORMER_V1_MEMBERS + " TEXT DEFAULT NULL);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");"
|
||||
};
|
||||
|
||||
private static final String[] GROUP_PROJECTION = {
|
||||
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
|
||||
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, FORMER_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
|
||||
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
|
||||
};
|
||||
|
||||
@@ -129,7 +134,7 @@ public final class GroupDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean findGroup(@NonNull GroupId groupId) {
|
||||
public boolean groupExists(@NonNull GroupId groupId) {
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?",
|
||||
new String[] {groupId.toString()},
|
||||
null, null, null))
|
||||
@@ -138,6 +143,27 @@ public final class GroupDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A gv1 group whose expected v2 ID matches the one provided.
|
||||
*/
|
||||
public Optional<GroupRecord> getGroupV1ByExpectedV2(@NonNull GroupId.V2 gv2Id) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
try (Cursor cursor = db.query(TABLE_NAME, GROUP_PROJECTION, EXPECTED_V2_ID + " = ?", SqlUtil.buildArgs(gv2Id), null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return getGroup(cursor);
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearFormerV1Members(@NonNull GroupId.V2 id) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull(FORMER_V1_MEMBERS);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(id));
|
||||
}
|
||||
|
||||
Optional<GroupRecord> getGroup(Cursor cursor) {
|
||||
Reader reader = new Reader(cursor);
|
||||
return Optional.fromNullable(reader.getCurrent());
|
||||
@@ -305,7 +331,7 @@ public final class GroupDatabase extends Database {
|
||||
|
||||
for (RecipientId member : currentMembers) {
|
||||
Recipient resolved = Recipient.resolved(member);
|
||||
if (memberSet.includeSelf || !resolved.isLocalNumber()) {
|
||||
if (memberSet.includeSelf || !resolved.isSelf()) {
|
||||
recipients.add(resolved);
|
||||
}
|
||||
}
|
||||
@@ -320,6 +346,9 @@ public final class GroupDatabase extends Database {
|
||||
@Nullable SignalServiceAttachmentPointer avatar,
|
||||
@Nullable String relay)
|
||||
{
|
||||
if (groupExists(groupId.deriveV2MigrationGroupId())) {
|
||||
throw new LegacyGroupInsertException(groupId);
|
||||
}
|
||||
create(groupId, title, members, avatar, relay, null, null);
|
||||
}
|
||||
|
||||
@@ -334,6 +363,10 @@ public final class GroupDatabase extends Database {
|
||||
{
|
||||
GroupId.V2 groupId = GroupId.v2(groupMasterKey);
|
||||
|
||||
if (getGroupV1ByExpectedV2(groupId).isPresent()) {
|
||||
throw new MissedGroupMigrationInsertException(groupId);
|
||||
}
|
||||
|
||||
create(groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState);
|
||||
|
||||
return groupId;
|
||||
@@ -376,6 +409,9 @@ public final class GroupDatabase extends Database {
|
||||
|
||||
if (groupId.isV2()) {
|
||||
contentValues.put(ACTIVE, groupState != null && gv2GroupActive(groupState) ? 1 : 0);
|
||||
} else if (groupId.isV1()) {
|
||||
contentValues.put(ACTIVE, 1);
|
||||
contentValues.put(EXPECTED_V2_ID, groupId.requireV1().deriveV2MigrationGroupId().toString());
|
||||
} else {
|
||||
contentValues.put(ACTIVE, 1);
|
||||
}
|
||||
@@ -434,6 +470,48 @@ public final class GroupDatabase extends Database {
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a V1 group to a V2 group.
|
||||
*/
|
||||
public @NonNull GroupId.V2 migrateToV2(@NonNull GroupId.V1 groupIdV1, @NonNull DecryptedGroup decryptedGroup) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
GroupId.V2 groupIdV2 = groupIdV1.deriveV2MigrationGroupId();
|
||||
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
GroupRecord record = getGroup(groupIdV1).get();
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(GROUP_ID, groupIdV2.toString());
|
||||
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
|
||||
contentValues.putNull(EXPECTED_V2_ID);
|
||||
|
||||
List<RecipientId> newMembers = Stream.of(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())).map(u -> RecipientId.from(u, null)).toList();
|
||||
newMembers.addAll(Stream.of(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList())).map(u -> RecipientId.from(u, null)).toList());
|
||||
|
||||
if (record.getMembers().size() > newMembers.size() || !newMembers.containsAll(record.getMembers())) {
|
||||
contentValues.put(FORMER_V1_MEMBERS, RecipientId.toSerializedList(record.getMembers()));
|
||||
}
|
||||
|
||||
int updated = db.update(TABLE_NAME, contentValues, GROUP_ID + " = ?", SqlUtil.buildArgs(groupIdV1.toString()));
|
||||
|
||||
if (updated != 1) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).updateGroupId(groupIdV1, groupIdV2);
|
||||
|
||||
update(groupMasterKey, decryptedGroup);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return groupIdV2;
|
||||
}
|
||||
|
||||
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
|
||||
update(GroupId.v2(groupMasterKey), decryptedGroup);
|
||||
}
|
||||
@@ -630,20 +708,21 @@ public final class GroupDatabase extends Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new GroupRecord(GroupId.parseOrThrow(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID))),
|
||||
RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(TITLE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)),
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1,
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1,
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(V2_MASTER_KEY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(V2_REVISION)),
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(V2_DECRYPTED_GROUP)));
|
||||
return new GroupRecord(GroupId.parseOrThrow(CursorUtil.requireString(cursor, GROUP_ID)),
|
||||
RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)),
|
||||
CursorUtil.requireString(cursor, TITLE),
|
||||
CursorUtil.requireString(cursor, MEMBERS),
|
||||
CursorUtil.requireString(cursor, FORMER_V1_MEMBERS),
|
||||
CursorUtil.requireLong(cursor, AVATAR_ID),
|
||||
CursorUtil.requireBlob(cursor, AVATAR_KEY),
|
||||
CursorUtil.requireString(cursor, AVATAR_CONTENT_TYPE),
|
||||
CursorUtil.requireString(cursor, AVATAR_RELAY),
|
||||
CursorUtil.requireBoolean(cursor, ACTIVE),
|
||||
CursorUtil.requireBlob(cursor, AVATAR_DIGEST),
|
||||
CursorUtil.requireBoolean(cursor, MMS),
|
||||
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
|
||||
CursorUtil.requireInt(cursor, V2_REVISION),
|
||||
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -659,6 +738,7 @@ public final class GroupDatabase extends Database {
|
||||
private final RecipientId recipientId;
|
||||
private final String title;
|
||||
private final List<RecipientId> members;
|
||||
private final List<RecipientId> formerV1Members;
|
||||
private final long avatarId;
|
||||
private final byte[] avatarKey;
|
||||
private final byte[] avatarDigest;
|
||||
@@ -668,10 +748,21 @@ public final class GroupDatabase extends Database {
|
||||
private final boolean mms;
|
||||
@Nullable private final V2GroupProperties v2GroupProperties;
|
||||
|
||||
public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members,
|
||||
long avatarId, byte[] avatarKey, String avatarContentType,
|
||||
String relay, boolean active, byte[] avatarDigest, boolean mms,
|
||||
@Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes)
|
||||
public GroupRecord(@NonNull GroupId id,
|
||||
@NonNull RecipientId recipientId,
|
||||
String title,
|
||||
String members,
|
||||
String formerV1Members,
|
||||
long avatarId,
|
||||
byte[] avatarKey,
|
||||
String avatarContentType,
|
||||
String relay,
|
||||
boolean active,
|
||||
byte[] avatarDigest,
|
||||
boolean mms,
|
||||
@Nullable byte[] groupMasterKeyBytes,
|
||||
int groupRevision,
|
||||
@Nullable byte[] decryptedGroupBytes)
|
||||
{
|
||||
this.id = id;
|
||||
this.recipientId = recipientId;
|
||||
@@ -696,8 +787,17 @@ public final class GroupDatabase extends Database {
|
||||
}
|
||||
this.v2GroupProperties = v2GroupProperties;
|
||||
|
||||
if (!TextUtils.isEmpty(members)) this.members = RecipientId.fromSerializedList(members);
|
||||
else this.members = new LinkedList<>();
|
||||
if (!TextUtils.isEmpty(members)) {
|
||||
this.members = RecipientId.fromSerializedList(members);
|
||||
} else {
|
||||
this.members = Collections.emptyList();
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(formerV1Members)) {
|
||||
this.formerV1Members = RecipientId.fromSerializedList(formerV1Members);
|
||||
} else {
|
||||
this.formerV1Members = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public GroupId getId() {
|
||||
@@ -712,10 +812,14 @@ public final class GroupDatabase extends Database {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<RecipientId> getMembers() {
|
||||
public @NonNull List<RecipientId> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> getFormerV1Members() {
|
||||
return formerV1Members;
|
||||
}
|
||||
|
||||
public boolean hasAvatar() {
|
||||
return avatarId != 0;
|
||||
}
|
||||
@@ -771,6 +875,8 @@ public final class GroupDatabase extends Database {
|
||||
public MemberLevel memberLevel(@NonNull Recipient recipient) {
|
||||
if (isV2Group()) {
|
||||
return requireV2GroupProperties().memberLevel(recipient);
|
||||
} else if (isMms() && recipient.isSelf()) {
|
||||
return MemberLevel.FULL_MEMBER;
|
||||
} else {
|
||||
return members.contains(recipient.getId()) ? MemberLevel.FULL_MEMBER
|
||||
: MemberLevel.NOT_A_MEMBER;
|
||||
@@ -950,4 +1056,16 @@ public final class GroupDatabase extends Database {
|
||||
return inGroup;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LegacyGroupInsertException extends IllegalStateException {
|
||||
public LegacyGroupInsertException(@Nullable GroupId id) {
|
||||
super("Tried to create a new GV1 entry when we already had a migrated GV2! " + id);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MissedGroupMigrationInsertException extends IllegalStateException {
|
||||
public MissedGroupMigrationInsertException(@Nullable GroupId id) {
|
||||
super("Tried to create a new GV2 entry when we already had a V1 group that mapped to the new ID! " + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,25 @@ public class MentionDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
void deleteMentionsForMessage(long messageId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = MESSAGE_ID + " = ?";
|
||||
|
||||
db.delete(TABLE_NAME, where, SqlUtil.buildArgs(messageId));
|
||||
}
|
||||
|
||||
void deleteAbandonedMentions() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = MESSAGE_ID + " NOT IN (SELECT " + MmsDatabase.ID + " FROM " + MmsDatabase.TABLE_NAME + ") OR " + THREAD_ID + " NOT IN (SELECT " + ThreadDatabase.ID + " FROM " + ThreadDatabase.TABLE_NAME + ")";
|
||||
|
||||
db.delete(TABLE_NAME, where, null);
|
||||
}
|
||||
|
||||
void deleteAllMentions() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
private @NonNull Map<Long, List<Mention>> readMentions(@Nullable Cursor cursor) {
|
||||
Map<Long, List<Mention>> mentions = new HashMap<>();
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
|
||||
@@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
@@ -24,6 +23,8 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class MentionUtil {
|
||||
|
||||
@@ -68,14 +69,13 @@ public final class MentionUtil {
|
||||
return new UpdatedBodyAndMentions(body, mentions);
|
||||
}
|
||||
|
||||
SortedSet<Mention> sortedMentions = new TreeSet<>(mentions);
|
||||
SpannableStringBuilder updatedBody = new SpannableStringBuilder();
|
||||
List<Mention> updatedMentions = new ArrayList<>();
|
||||
|
||||
Collections.sort(mentions);
|
||||
|
||||
int bodyIndex = 0;
|
||||
|
||||
for (Mention mention : mentions) {
|
||||
for (Mention mention : sortedMentions) {
|
||||
updatedBody.append(body.subSequence(bodyIndex, mention.getStart()));
|
||||
CharSequence replaceWith = replacementTextGenerator.apply(mention);
|
||||
Mention updatedMention = new Mention(mention.getRecipientId(), updatedBody.length(), replaceWith.length());
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -83,6 +84,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract boolean hasReceivedAnyCallsSince(long threadId, long timestamp);
|
||||
public abstract @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage();
|
||||
public abstract boolean isSent(long messageId);
|
||||
public abstract List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp);
|
||||
|
||||
public abstract void markExpireStarted(long messageId);
|
||||
public abstract void markExpireStarted(long messageId, long startTime);
|
||||
@@ -108,7 +110,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract void markUnidentified(long messageId, boolean unidentified);
|
||||
public abstract void markAsSending(long messageId);
|
||||
public abstract void markAsRemoteDelete(long messageId);
|
||||
public abstract void markAsMissedCall(long id);
|
||||
public abstract void markAsMissedCall(long id, boolean isVideoOffer);
|
||||
public abstract void markAsNotified(long id);
|
||||
public abstract void markSmsStatus(long id, int status);
|
||||
public abstract void markDownloadState(long messageId, long state);
|
||||
@@ -124,9 +126,9 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract void addFailures(long messageId, List<NetworkFailure> failure);
|
||||
public abstract void removeFailure(long messageId, NetworkFailure failure);
|
||||
|
||||
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address);
|
||||
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address);
|
||||
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp);
|
||||
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer);
|
||||
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer);
|
||||
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer);
|
||||
|
||||
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type);
|
||||
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message);
|
||||
@@ -137,6 +139,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
||||
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
||||
public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName);
|
||||
public abstract void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients);
|
||||
|
||||
public abstract boolean deleteMessage(long messageId);
|
||||
abstract void deleteThread(long threadId);
|
||||
@@ -145,6 +148,8 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
abstract void deleteAllThreads();
|
||||
abstract void deleteAbandonedMessages();
|
||||
|
||||
public abstract List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit);
|
||||
|
||||
public abstract SQLiteDatabase beginTransaction();
|
||||
public abstract void endTransaction(SQLiteDatabase database);
|
||||
public abstract void setTransactionSuccessful();
|
||||
|
||||
@@ -82,6 +82,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -364,7 +365,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsMissedCall(long id) {
|
||||
public void markAsMissedCall(long id, boolean isVideoOffer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@@ -379,17 +380,17 @@ public class MmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address) {
|
||||
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address) {
|
||||
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp) {
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@@ -413,6 +414,11 @@ public class MmsDatabase extends MessageDatabase {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTransaction(SQLiteDatabase database) {
|
||||
database.endTransaction();
|
||||
@@ -587,7 +593,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
|
||||
private long getThreadIdFor(@NonNull IncomingMediaMessage retrieved) {
|
||||
if (retrieved.getGroupId() != null) {
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(retrieved.getGroupId());
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(retrieved.getGroupId());
|
||||
Recipient groupRecipients = Recipient.resolved(groupRecipientId);
|
||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
|
||||
} else {
|
||||
@@ -605,11 +611,25 @@ public class MmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) {
|
||||
return rawQuery(where, arguments, false, 0);
|
||||
}
|
||||
|
||||
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
return database.rawQuery("SELECT " + Util.join(MMS_PROJECTION, ",") +
|
||||
" FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME +
|
||||
" ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" +
|
||||
" WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, arguments);
|
||||
String rawQueryString = "SELECT " + Util.join(MMS_PROJECTION, ",") +
|
||||
" FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME +
|
||||
" ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" +
|
||||
" WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID;
|
||||
|
||||
if (reverse) {
|
||||
rawQueryString += " ORDER BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " DESC";
|
||||
}
|
||||
|
||||
if (limit > 0) {
|
||||
rawQueryString += " LIMIT " + limit;
|
||||
}
|
||||
|
||||
return database.rawQuery(rawQueryString, arguments);
|
||||
}
|
||||
|
||||
private Cursor internalGetMessage(long messageId) {
|
||||
@@ -704,6 +724,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) });
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentsForMessage(messageId);
|
||||
DatabaseFactory.getMentionDatabase(context).deleteMentionsForMessage(messageId);
|
||||
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
|
||||
@@ -934,6 +955,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseFactory.getMentionDatabase(context).deleteAbandonedMentions();
|
||||
|
||||
try (Cursor cursor = database.query(ThreadDatabase.TABLE_NAME, new String[] { ThreadDatabase.ID }, ThreadDatabase.EXPIRES_IN + " > 0", null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
DatabaseFactory.getThreadDatabase(context).update(cursor.getLong(0), false);
|
||||
@@ -1396,7 +1419,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
|
||||
|
||||
boolean mentionsSelf = Stream.of(mentions).filter(m -> Recipient.resolved(m.getRecipientId()).isLocalNumber()).findFirst().isPresent();
|
||||
boolean mentionsSelf = Stream.of(mentions).filter(m -> Recipient.resolved(m.getRecipientId()).isSelf()).findFirst().isPresent();
|
||||
|
||||
List<Attachment> allAttachments = new LinkedList<>();
|
||||
List<Attachment> contactAttachments = Stream.of(sharedContacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList();
|
||||
@@ -1460,6 +1483,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
|
||||
@Override
|
||||
public boolean deleteMessage(long messageId) {
|
||||
Log.d(TAG, "deleteMessage(" + messageId + ")");
|
||||
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
attachmentDatabase.deleteAttachmentsForMessage(messageId);
|
||||
@@ -1467,6 +1492,9 @@ public class MmsDatabase extends MessageDatabase {
|
||||
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
groupReceiptDatabase.deleteRowsForMessage(messageId);
|
||||
|
||||
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
|
||||
mentionDatabase.deleteMentionsForMessage(messageId);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
||||
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
|
||||
@@ -1478,6 +1506,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
|
||||
@Override
|
||||
public void deleteThread(long threadId) {
|
||||
Log.d(TAG, "deleteThread(" + threadId + ")");
|
||||
Set<Long> singleThreadSet = new HashSet<>();
|
||||
singleThreadSet.add(threadId);
|
||||
deleteThreads(singleThreadSet);
|
||||
@@ -1556,8 +1585,15 @@ public class MmsDatabase extends MessageDatabase {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
void deleteThreads(@NonNull Set<Long> threadIds) {
|
||||
Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")");
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = "";
|
||||
Cursor cursor = null;
|
||||
@@ -1597,10 +1633,29 @@ public class MmsDatabase extends MessageDatabase {
|
||||
db.delete(TABLE_NAME, where, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) {
|
||||
String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " +
|
||||
TABLE_NAME + "." + getDateReceivedColumnName() + " >= ?";
|
||||
String[] args = SqlUtil.buildArgs(threadId, timestamp);
|
||||
|
||||
try (Reader reader = readerFor(rawQuery(where, args, false, limit))) {
|
||||
List<MessageRecord> results = new ArrayList<>(reader.cursor.getCount());
|
||||
|
||||
while (reader.getNext() != null) {
|
||||
results.add(reader.getCurrent());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAllThreads() {
|
||||
Log.d(TAG, "deleteAllThreads()");
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments();
|
||||
DatabaseFactory.getGroupReceiptDatabase(context).deleteAllRows();
|
||||
DatabaseFactory.getMentionDatabase(context).deleteAllMentions();
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, null, null);
|
||||
|
||||
@@ -32,13 +32,17 @@ public interface MmsSmsColumns {
|
||||
// Base Types
|
||||
protected static final long BASE_TYPE_MASK = 0x1F;
|
||||
|
||||
protected static final long INCOMING_CALL_TYPE = 1;
|
||||
protected static final long OUTGOING_CALL_TYPE = 2;
|
||||
protected static final long MISSED_CALL_TYPE = 3;
|
||||
protected static final long INCOMING_AUDIO_CALL_TYPE = 1;
|
||||
protected static final long OUTGOING_AUDIO_CALL_TYPE = 2;
|
||||
protected static final long MISSED_AUDIO_CALL_TYPE = 3;
|
||||
protected static final long JOINED_TYPE = 4;
|
||||
protected static final long UNSUPPORTED_MESSAGE_TYPE = 5;
|
||||
protected static final long INVALID_MESSAGE_TYPE = 6;
|
||||
protected static final long PROFILE_CHANGE_TYPE = 7;
|
||||
protected static final long MISSED_VIDEO_CALL_TYPE = 8;
|
||||
protected static final long GV1_MIGRATION_TYPE = 9;
|
||||
protected static final long INCOMING_VIDEO_CALL_TYPE = 10;
|
||||
protected static final long OUTGOING_VIDEO_CALL_TYPE = 11;
|
||||
|
||||
protected static final long BASE_INBOX_TYPE = 20;
|
||||
protected static final long BASE_OUTBOX_TYPE = 21;
|
||||
@@ -53,7 +57,7 @@ public interface MmsSmsColumns {
|
||||
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
|
||||
BASE_PENDING_SECURE_SMS_FALLBACK,
|
||||
BASE_PENDING_INSECURE_SMS_FALLBACK,
|
||||
OUTGOING_CALL_TYPE};
|
||||
OUTGOING_AUDIO_CALL_TYPE, OUTGOING_VIDEO_CALL_TYPE};
|
||||
|
||||
// Message attributes
|
||||
protected static final long MESSAGE_ATTRIBUTE_MASK = 0xE0;
|
||||
@@ -204,23 +208,41 @@ public interface MmsSmsColumns {
|
||||
}
|
||||
|
||||
public static boolean isCallLog(long type) {
|
||||
return type == INCOMING_CALL_TYPE || type == OUTGOING_CALL_TYPE || type == MISSED_CALL_TYPE;
|
||||
return isIncomingAudioCall(type) ||
|
||||
isIncomingVideoCall(type) ||
|
||||
isOutgoingAudioCall(type) ||
|
||||
isOutgoingVideoCall(type) ||
|
||||
isMissedAudioCall(type) ||
|
||||
isMissedVideoCall(type);
|
||||
}
|
||||
|
||||
public static boolean isExpirationTimerUpdate(long type) {
|
||||
return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0;
|
||||
}
|
||||
|
||||
public static boolean isIncomingCall(long type) {
|
||||
return type == INCOMING_CALL_TYPE;
|
||||
public static boolean isIncomingAudioCall(long type) {
|
||||
return type == INCOMING_AUDIO_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isOutgoingCall(long type) {
|
||||
return type == OUTGOING_CALL_TYPE;
|
||||
public static boolean isIncomingVideoCall(long type) {
|
||||
return type == INCOMING_VIDEO_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isMissedCall(long type) {
|
||||
return type == MISSED_CALL_TYPE;
|
||||
public static boolean isOutgoingAudioCall(long type) {
|
||||
return type == OUTGOING_AUDIO_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isOutgoingVideoCall(long type) {
|
||||
return type == OUTGOING_VIDEO_CALL_TYPE;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isMissedAudioCall(long type) {
|
||||
return type == MISSED_AUDIO_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isMissedVideoCall(long type) {
|
||||
return type == MISSED_VIDEO_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isGroupUpdate(long type) {
|
||||
@@ -260,6 +282,10 @@ public interface MmsSmsColumns {
|
||||
return type == PROFILE_CHANGE_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isGroupV1MigrationEvent(long type) {
|
||||
return type == GV1_MIGRATION_TYPE;
|
||||
}
|
||||
|
||||
public static long translateFromSystemBaseType(long theirType) {
|
||||
// public static final int NONE_TYPE = 0;
|
||||
// public static final int INBOX_TYPE = 1;
|
||||
|
||||
@@ -22,18 +22,23 @@ import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MmsSmsDatabase extends Database {
|
||||
@@ -147,8 +152,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MessageRecord messageRecord;
|
||||
|
||||
while ((messageRecord = reader.getNext()) != null) {
|
||||
if ((Recipient.resolved(author).isLocalNumber() && messageRecord.isOutgoing()) ||
|
||||
(!Recipient.resolved(author).isLocalNumber() && messageRecord.getIndividualRecipient().getId().equals(author)))
|
||||
if ((Recipient.resolved(author).isSelf() && messageRecord.isOutgoing()) ||
|
||||
(!Recipient.resolved(author).isSelf() && messageRecord.getIndividualRecipient().getId().equals(author)))
|
||||
{
|
||||
return messageRecord;
|
||||
}
|
||||
@@ -158,6 +163,18 @@ public class MmsSmsDatabase extends Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull List<MessageRecord> getMessagesAfterVoiceNoteInclusive(long messageId, long limit) throws NoSuchMessageException {
|
||||
MessageRecord origin = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
List<MessageRecord> mms = DatabaseFactory.getMmsDatabase(context).getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit);
|
||||
List<MessageRecord> sms = DatabaseFactory.getSmsDatabase(context).getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit);
|
||||
|
||||
mms.addAll(sms);
|
||||
Collections.sort(mms, (a, b) -> Long.compare(a.getDateReceived(), b.getDateReceived()));
|
||||
|
||||
return Stream.of(mms).limit(limit).toList();
|
||||
}
|
||||
|
||||
|
||||
public Cursor getConversation(long threadId, long offset, long limit) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
@@ -314,10 +331,10 @@ public class MmsSmsDatabase extends Database {
|
||||
|
||||
public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull RecipientId recipientId) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.REMOTE_DELETED + " = 0";
|
||||
|
||||
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.RECIPIENT_ID}, selection, order, null)) {
|
||||
boolean isOwnNumber = Recipient.resolved(recipientId).isLocalNumber();
|
||||
boolean isOwnNumber = Recipient.resolved(recipientId).isSelf();
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
boolean quoteIdMatches = cursor.getLong(0) == quoteId;
|
||||
@@ -333,10 +350,10 @@ public class MmsSmsDatabase extends Database {
|
||||
|
||||
public int getMessagePositionInConversation(long threadId, long receivedTimestamp, @NonNull RecipientId recipientId) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.REMOTE_DELETED + " = 0";
|
||||
|
||||
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.RECIPIENT_ID}, selection, order, null)) {
|
||||
boolean isOwnNumber = Recipient.resolved(recipientId).isLocalNumber();
|
||||
boolean isOwnNumber = Recipient.resolved(recipientId).isSelf();
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
boolean timestampMatches = cursor.getLong(0) == receivedTimestamp;
|
||||
@@ -364,7 +381,9 @@ public class MmsSmsDatabase extends Database {
|
||||
*/
|
||||
public int getMessagePositionInConversation(long threadId, long receivedTimestamp) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + receivedTimestamp;
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " +
|
||||
MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + receivedTimestamp + " AND " +
|
||||
MmsSmsColumns.REMOTE_DELETED + " = 0";
|
||||
|
||||
try (Cursor cursor = queryTables(new String[]{ "COUNT(*)" }, selection, order, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
@@ -388,11 +407,13 @@ public class MmsSmsDatabase extends Database {
|
||||
}
|
||||
|
||||
public void deleteMessagesInThreadBeforeDate(long threadId, long trimBeforeDate) {
|
||||
Log.d(TAG, "deleteMessagesInThreadBeforeData(" + threadId + ", " + trimBeforeDate + ")");
|
||||
DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate);
|
||||
DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate);
|
||||
}
|
||||
|
||||
public void deleteAbandonedMessages() {
|
||||
Log.d(TAG, "deleteAbandonedMessages()");
|
||||
DatabaseFactory.getSmsDatabase(context).deleteAbandonedMessages();
|
||||
DatabaseFactory.getMmsDatabase(context).deleteAbandonedMessages();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import net.sqlcipher.database.SQLiteConstraintException;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
@@ -23,6 +22,7 @@ import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -31,17 +31,20 @@ import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Bitmask;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
@@ -62,6 +65,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -113,8 +117,7 @@ public class RecipientDatabase extends Database {
|
||||
private static final String LAST_PROFILE_FETCH = "last_profile_fetch";
|
||||
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
|
||||
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
|
||||
private static final String UUID_CAPABILITY = "uuid_supported";
|
||||
private static final String GROUPS_V2_CAPABILITY = "gv2_capability";
|
||||
private static final String CAPABILITIES = "capabilities";
|
||||
private static final String STORAGE_SERVICE_ID = "storage_service_key";
|
||||
private static final String DIRTY = "dirty";
|
||||
private static final String PROFILE_GIVEN_NAME = "signal_profile_name";
|
||||
@@ -128,8 +131,15 @@ public class RecipientDatabase extends Database {
|
||||
private static final String IDENTITY_STATUS = "identity_status";
|
||||
private static final String IDENTITY_KEY = "identity_key";
|
||||
|
||||
private static final class Capabilities {
|
||||
static final int BIT_LENGTH = 2;
|
||||
|
||||
static final int GROUPS_V2 = 0;
|
||||
static final int GROUPS_V1_MIGRATION = 1;
|
||||
}
|
||||
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
|
||||
ID, UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
|
||||
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
|
||||
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
|
||||
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
|
||||
@@ -137,7 +147,7 @@ public class RecipientDatabase extends Database {
|
||||
NOTIFICATION_CHANNEL,
|
||||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION,
|
||||
UUID_CAPABILITY, GROUPS_V2_CAPABILITY,
|
||||
CAPABILITIES,
|
||||
STORAGE_SERVICE_ID, DIRTY,
|
||||
MENTION_SETTING
|
||||
};
|
||||
@@ -145,20 +155,13 @@ public class RecipientDatabase extends Database {
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME};
|
||||
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
|
||||
static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
private static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
.map(columnName -> TABLE_NAME + "." + columnName)
|
||||
.toList().toArray(new String[0]);
|
||||
|
||||
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
|
||||
static final String[] TYPED_RECIPIENT_PROJECTION_NO_ID = Arrays.copyOfRange(TYPED_RECIPIENT_PROJECTION, 1, TYPED_RECIPIENT_PROJECTION.length);
|
||||
|
||||
private static final String[] RECIPIENT_FULL_PROJECTION = Stream.of(
|
||||
new String[] { TABLE_NAME + "." + ID,
|
||||
TABLE_NAME + "." + STORAGE_PROTO },
|
||||
TYPED_RECIPIENT_PROJECTION,
|
||||
new String[] {
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
|
||||
}).flatMap(Stream::of).toArray(String[]::new);
|
||||
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
|
||||
|
||||
public static final String[] CREATE_INDEXS = new String[] {
|
||||
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
|
||||
@@ -335,12 +338,11 @@ public class RecipientDatabase extends Database {
|
||||
LAST_PROFILE_FETCH + " INTEGER DEFAULT 0, " +
|
||||
UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
|
||||
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
|
||||
UUID_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
|
||||
GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
|
||||
STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " +
|
||||
MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ", " +
|
||||
STORAGE_PROTO + " TEXT DEFAULT NULL);";
|
||||
STORAGE_PROTO + " TEXT DEFAULT NULL, " +
|
||||
CAPABILITIES + " INTEGER DEFAULT 0);";
|
||||
|
||||
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
|
||||
" FROM " + TABLE_NAME +
|
||||
@@ -376,8 +378,8 @@ public class RecipientDatabase extends Database {
|
||||
return getByColumn(EMAIL, email);
|
||||
}
|
||||
|
||||
public @NonNull Optional<RecipientId> getByGroupId(@NonNull String groupId) {
|
||||
return getByColumn(GROUP_ID, groupId);
|
||||
public @NonNull Optional<RecipientId> getByGroupId(@NonNull GroupId groupId) {
|
||||
return getByColumn(GROUP_ID, groupId.toString());
|
||||
|
||||
}
|
||||
|
||||
@@ -509,6 +511,7 @@ public class RecipientDatabase extends Database {
|
||||
if (transactionSuccessful) {
|
||||
if (recipientNeedingRefresh != null) {
|
||||
Recipient.live(recipientNeedingRefresh).refresh();
|
||||
RetrieveProfileJob.enqueue(recipientNeedingRefresh);
|
||||
}
|
||||
|
||||
if (remapped != null) {
|
||||
@@ -552,27 +555,94 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull GroupId groupId) {
|
||||
GetOrInsertResult result = getOrInsertByColumn(GROUP_ID, groupId.toString());
|
||||
Optional<RecipientId> existing = getByGroupId(groupId);
|
||||
|
||||
if (result.neededInsert) {
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
} else if (groupId.isV1() && DatabaseFactory.getGroupDatabase(context).groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
|
||||
throw new GroupDatabase.LegacyGroupInsertException(groupId);
|
||||
} else if (groupId.isV2() && DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2()).isPresent()) {
|
||||
throw new GroupDatabase.MissedGroupMigrationInsertException(groupId);
|
||||
} else {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(GROUP_ID, groupId.toString());
|
||||
|
||||
if (groupId.isMms()) {
|
||||
values.put(GROUP_TYPE, GroupType.MMS.getId());
|
||||
} else {
|
||||
if (groupId.isV2()) {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
long id = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
|
||||
|
||||
if (id < 0) {
|
||||
existing = getByColumn(GROUP_ID, groupId.toString());
|
||||
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
} else if (groupId.isV1() && DatabaseFactory.getGroupDatabase(context).groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
|
||||
throw new GroupDatabase.LegacyGroupInsertException(groupId);
|
||||
} else if (groupId.isV2() && DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2()).isPresent()) {
|
||||
throw new GroupDatabase.MissedGroupMigrationInsertException(groupId);
|
||||
} else {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
throw new AssertionError("Failed to insert recipient!");
|
||||
}
|
||||
values.put(DIRTY, DirtyState.INSERT.getId());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
} else {
|
||||
ContentValues groupUpdates = new ContentValues();
|
||||
|
||||
if (groupId.isMms()) {
|
||||
groupUpdates.put(GROUP_TYPE, GroupType.MMS.getId());
|
||||
} else {
|
||||
if (groupId.isV2()) {
|
||||
groupUpdates.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
} else {
|
||||
groupUpdates.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
}
|
||||
groupUpdates.put(DIRTY, DirtyState.INSERT.getId());
|
||||
groupUpdates.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
}
|
||||
|
||||
RecipientId recipientId = RecipientId.from(id);
|
||||
|
||||
update(recipientId, groupUpdates);
|
||||
|
||||
return recipientId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Recipient#externalPossiblyMigratedGroup(Context, GroupId)}.
|
||||
*/
|
||||
public @NonNull RecipientId getOrInsertFromPossiblyMigratedGroupId(@NonNull GroupId groupId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
Optional<RecipientId> existing = getByColumn(GROUP_ID, groupId.toString());
|
||||
|
||||
if (existing.isPresent()) {
|
||||
db.setTransactionSuccessful();
|
||||
return existing.get();
|
||||
}
|
||||
|
||||
update(result.recipientId, values);
|
||||
}
|
||||
if (groupId.isV1()) {
|
||||
Optional<RecipientId> v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId());
|
||||
if (v2.isPresent()) {
|
||||
db.setTransactionSuccessful();
|
||||
return v2.get();
|
||||
}
|
||||
}
|
||||
|
||||
return result.recipientId;
|
||||
if (groupId.isV2()) {
|
||||
Optional<GroupDatabase.GroupRecord> v1 = DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2());
|
||||
if (v1.isPresent()) {
|
||||
db.setTransactionSuccessful();
|
||||
return v1.get().getRecipientId();
|
||||
}
|
||||
}
|
||||
|
||||
RecipientId id = getOrInsertFromGroupId(groupId);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
return id;
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public Cursor getBlocked() {
|
||||
@@ -596,11 +666,10 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
public @NonNull RecipientSettings getRecipientSettings(@NonNull RecipientId id) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID;
|
||||
String query = TABLE_NAME + "." + ID + " = ?";
|
||||
String query = ID + " = ?";
|
||||
String[] args = new String[] { id.serialize() };
|
||||
|
||||
try (Cursor cursor = database.query(table, RECIPIENT_FULL_PROJECTION, query, args, null, null, null)) {
|
||||
try (Cursor cursor = database.query(TABLE_NAME, RECIPIENT_PROJECTION, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return getRecipientSettings(context, cursor);
|
||||
} else {
|
||||
@@ -675,6 +744,20 @@ public class RecipientDatabase extends Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void markNeedsSync(@NonNull Collection<RecipientId> recipientIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (RecipientId recipientId : recipientIds) {
|
||||
markDirty(recipientId, DirtyState.UPDATE);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void markNeedsSync(@NonNull RecipientId recipientId) {
|
||||
markDirty(recipientId, DirtyState.UPDATE);
|
||||
}
|
||||
@@ -697,6 +780,10 @@ public class RecipientDatabase extends Database {
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
for (RecipientId id : storageIds.keySet()) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
|
||||
@@ -771,7 +858,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
threadDatabase.setArchived(recipientId, insert.isArchived());
|
||||
threadDatabase.applyStorageSyncUpdate(recipientId, insert);
|
||||
needsRefresh.add(recipientId);
|
||||
}
|
||||
|
||||
@@ -811,12 +898,12 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
|
||||
|
||||
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED) &&
|
||||
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED))
|
||||
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) &&
|
||||
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED))
|
||||
{
|
||||
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
|
||||
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED) &&
|
||||
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED))
|
||||
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) &&
|
||||
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
|
||||
{
|
||||
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true);
|
||||
}
|
||||
@@ -824,16 +911,16 @@ public class RecipientDatabase extends Database {
|
||||
Log.w(TAG, "Failed to process identity key during update! Skipping.", e);
|
||||
}
|
||||
|
||||
threadDatabase.setArchived(recipientId, update.getNew().isArchived());
|
||||
threadDatabase.applyStorageSyncUpdate(recipientId, update.getNew());
|
||||
needsRefresh.add(recipientId);
|
||||
}
|
||||
|
||||
for (SignalGroupV1Record insert : groupV1Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(insert.getGroupId()));
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(insert.getGroupId()));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
|
||||
needsRefresh.add(recipient.getId());
|
||||
}
|
||||
|
||||
@@ -845,21 +932,26 @@ public class RecipientDatabase extends Database {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(update.getOld().getGroupId()));
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId()));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
||||
needsRefresh.add(recipient.getId());
|
||||
}
|
||||
|
||||
for (SignalGroupV2Record insert : groupV2Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV2(insert));
|
||||
|
||||
GroupMasterKey masterKey = insert.getMasterKeyOrThrow();
|
||||
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
||||
ContentValues values = getValuesForStorageGroupV2(insert);
|
||||
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
Recipient recipient = Recipient.externalGroupExact(context, groupId);
|
||||
|
||||
if (id < 0) {
|
||||
Log.w(TAG, String.format("Recipient %s is already linked to group %s", recipient.getId(), groupId));
|
||||
} else {
|
||||
Log.i(TAG, String.format("Inserted recipient %s for group %s", recipient.getId(), groupId));
|
||||
}
|
||||
|
||||
Log.i(TAG, "Creating restore placeholder for " + groupId);
|
||||
|
||||
DatabaseFactory.getGroupDatabase(context)
|
||||
.create(masterKey,
|
||||
DecryptedGroup.newBuilder()
|
||||
@@ -870,7 +962,7 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
|
||||
needsRefresh.add(recipient.getId());
|
||||
}
|
||||
|
||||
@@ -883,9 +975,9 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(masterKey));
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
||||
needsRefresh.add(recipient.getId());
|
||||
}
|
||||
|
||||
@@ -934,6 +1026,8 @@ public class RecipientDatabase extends Database {
|
||||
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
|
||||
}
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(Recipient.self().getId(), update);
|
||||
|
||||
Recipient.self().live().refresh();
|
||||
}
|
||||
|
||||
@@ -1046,14 +1140,22 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
private List<RecipientSettings> getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
|
||||
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID;
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
|
||||
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID
|
||||
+ " LEFT OUTER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
|
||||
List<RecipientSettings> out = new ArrayList<>();
|
||||
|
||||
String[] columns = Stream.of(RECIPIENT_FULL_PROJECTION,
|
||||
new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY }).flatMap(Stream::of).toArray(String[]::new);
|
||||
String[] columns = Stream.of(TYPED_RECIPIENT_PROJECTION,
|
||||
new String[]{ RecipientDatabase.TABLE_NAME + "." + STORAGE_PROTO,
|
||||
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY,
|
||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ARCHIVED,
|
||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.READ,
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY })
|
||||
.flatMap(Stream::of)
|
||||
.toArray(String[]::new);
|
||||
|
||||
try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) {
|
||||
try (Cursor cursor = db.query(table, columns, query, args, TABLE_NAME + "." + ID, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
out.add(getRecipientSettings(context, cursor));
|
||||
}
|
||||
@@ -1094,7 +1196,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
for (GroupId.V2 id : DatabaseFactory.getGroupDatabase(context).getAllGroupV2Ids()) {
|
||||
Recipient recipient = Recipient.externalGroup(context, id);
|
||||
Recipient recipient = Recipient.externalGroupExact(context, id);
|
||||
RecipientId recipientId = recipient.getId();
|
||||
RecipientSettings recipientSettingsForSync = getRecipientSettingsForSync(recipientId);
|
||||
|
||||
@@ -1147,27 +1249,9 @@ public class RecipientDatabase extends Database {
|
||||
String notificationChannel = CursorUtil.requireString(cursor, NOTIFICATION_CHANNEL);
|
||||
int unidentifiedAccessMode = CursorUtil.requireInt(cursor, UNIDENTIFIED_ACCESS_MODE);
|
||||
boolean forceSmsSelection = CursorUtil.requireBoolean(cursor, FORCE_SMS_SELECTION);
|
||||
int uuidCapabilityValue = CursorUtil.requireInt(cursor, UUID_CAPABILITY);
|
||||
int groupsV2CapabilityValue = CursorUtil.requireInt(cursor, GROUPS_V2_CAPABILITY);
|
||||
long capabilities = CursorUtil.requireLong(cursor, CAPABILITIES);
|
||||
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
|
||||
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
|
||||
String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull();
|
||||
|
||||
Optional<String> identityKeyRaw = CursorUtil.getString(cursor, IDENTITY_KEY);
|
||||
Optional<Integer> identityStatusRaw = CursorUtil.getInt(cursor, IDENTITY_STATUS);
|
||||
|
||||
int masterKeyIndex = cursor.getColumnIndex(GroupDatabase.V2_MASTER_KEY);
|
||||
GroupMasterKey groupMasterKey = null;
|
||||
try {
|
||||
if (masterKeyIndex != -1) {
|
||||
byte[] blob = cursor.getBlob(masterKeyIndex);
|
||||
if (blob != null) {
|
||||
groupMasterKey = new GroupMasterKey(blob);
|
||||
}
|
||||
}
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
MaterialColor color;
|
||||
byte[] profileKey = null;
|
||||
@@ -1198,30 +1282,57 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
|
||||
byte[] identityKey = identityKeyRaw.transform(Base64::decodeOrThrow).orNull();
|
||||
byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null;
|
||||
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
|
||||
|
||||
IdentityDatabase.VerifiedStatus identityStatus = identityStatusRaw.transform(IdentityDatabase.VerifiedStatus::forState).or(IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
|
||||
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, groupMasterKey, GroupType.fromId(groupType), blocked, muteUntil,
|
||||
return new RecipientSettings(RecipientId.from(id),
|
||||
uuid,
|
||||
username,
|
||||
e164,
|
||||
email,
|
||||
groupId,
|
||||
GroupType.fromId(groupType),
|
||||
blocked,
|
||||
muteUntil,
|
||||
VibrateState.fromId(messageVibrateState),
|
||||
VibrateState.fromId(callVibrateState),
|
||||
Util.uri(messageRingtone), Util.uri(callRingtone),
|
||||
color, defaultSubscriptionId, expireMessages,
|
||||
Util.uri(messageRingtone),
|
||||
Util.uri(callRingtone),
|
||||
color,
|
||||
defaultSubscriptionId,
|
||||
expireMessages,
|
||||
RegisteredState.fromId(registeredState),
|
||||
profileKey, profileKeyCredential,
|
||||
systemDisplayName, systemContactPhoto,
|
||||
systemPhoneLabel, systemContactUri,
|
||||
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar,
|
||||
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing, lastProfileFetch,
|
||||
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
profileKey,
|
||||
profileKeyCredential,
|
||||
systemDisplayName,
|
||||
systemContactPhoto,
|
||||
systemPhoneLabel,
|
||||
systemContactUri,
|
||||
ProfileName.fromParts(profileGivenName, profileFamilyName),
|
||||
signalProfileAvatar,
|
||||
AvatarHelper.hasAvatar(context, RecipientId.from(id)),
|
||||
profileSharing,
|
||||
lastProfileFetch,
|
||||
notificationChannel,
|
||||
UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
forceSmsSelection,
|
||||
Recipient.Capability.deserialize(uuidCapabilityValue),
|
||||
Recipient.Capability.deserialize(groupsV2CapabilityValue),
|
||||
capabilities,
|
||||
InsightsBannerTier.fromId(insightsBannerTier),
|
||||
storageKey, identityKey, identityStatus, MentionSetting.fromId(mentionSettingId),
|
||||
storageProto);
|
||||
storageKey,
|
||||
MentionSetting.fromId(mentionSettingId),
|
||||
getSyncExtras(cursor));
|
||||
}
|
||||
|
||||
private static @NonNull RecipientSettings.SyncExtras getSyncExtras(@NonNull Cursor cursor) {
|
||||
String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull();
|
||||
byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null;
|
||||
boolean archived = CursorUtil.getBoolean(cursor, ThreadDatabase.ARCHIVED).or(false);
|
||||
boolean forcedUnread = CursorUtil.getInt(cursor, ThreadDatabase.READ).transform(status -> status == ThreadDatabase.ReadStatus.FORCED_UNREAD.serialize()).or(false);
|
||||
GroupMasterKey groupMasterKey = CursorUtil.getBlob(cursor, GroupDatabase.V2_MASTER_KEY).transform(GroupUtil::requireMasterKey).orNull();
|
||||
byte[] identityKey = CursorUtil.getString(cursor, IDENTITY_KEY).transform(Base64::decodeOrThrow).orNull();
|
||||
VerifiedStatus identityStatus = CursorUtil.getInt(cursor, IDENTITY_STATUS).transform(VerifiedStatus::forState).or(VerifiedStatus.DEFAULT);
|
||||
|
||||
|
||||
return new RecipientSettings.SyncExtras(storageProto, groupMasterKey, identityKey, identityStatus, archived, forcedUnread);
|
||||
}
|
||||
|
||||
public BulkOperationsHandle beginBulkSystemContactUpdate() {
|
||||
@@ -1367,9 +1478,14 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
public void setCapabilities(@NonNull RecipientId id, @NonNull SignalServiceProfile.Capabilities capabilities) {
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(UUID_CAPABILITY, Recipient.Capability.fromBoolean(capabilities.isUuid()).serialize());
|
||||
values.put(GROUPS_V2_CAPABILITY, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize());
|
||||
long value = 0;
|
||||
|
||||
value = Bitmask.update(value, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize());
|
||||
value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration()).serialize());
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(CAPABILITIES, value);
|
||||
|
||||
if (update(id, values)) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
@@ -1402,7 +1518,7 @@ public class RecipientDatabase extends Database {
|
||||
valuesToSet.putNull(PROFILE_KEY_CREDENTIAL);
|
||||
valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode());
|
||||
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare);
|
||||
SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare);
|
||||
|
||||
if (update(updateQuery, valuesToSet)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
@@ -1451,7 +1567,7 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(profileKeyCredential.serialize()));
|
||||
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
|
||||
if (update(updateQuery, values)) {
|
||||
// TODO [greyson] If we sync this in future, mark dirty
|
||||
@@ -1519,6 +1635,27 @@ public class RecipientDatabase extends Database {
|
||||
return updated;
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> getSimilarRecipientIds(@NonNull Recipient recipient) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = SqlUtil.buildArgs(ID, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ") AS checked_name");
|
||||
String where = "checked_name = ?";
|
||||
|
||||
String[] arguments = SqlUtil.buildArgs(recipient.getProfileName().toString());
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, projection, where, arguments, null, null, null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<RecipientId> results = new ArrayList<>(cursor.getCount());
|
||||
while (cursor.moveToNext()) {
|
||||
results.add(RecipientId.from(CursorUtil.requireLong(cursor, ID)));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
public void setProfileName(@NonNull RecipientId id, @NonNull ProfileName profileName) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
@@ -2086,7 +2223,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
return Stream.of(recipientsWithinInteractionThreshold)
|
||||
.filterNot(Recipient::isLocalNumber)
|
||||
.filterNot(Recipient::isSelf)
|
||||
.filter(r -> r.getLastProfileFetchTime() < lastProfileFetchThreshold)
|
||||
.limit(limit)
|
||||
.map(Recipient::getId)
|
||||
@@ -2168,6 +2305,10 @@ public class RecipientDatabase extends Database {
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
for (RecipientId id : keys.keySet()) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearDirtyState(@NonNull List<RecipientId> recipients) {
|
||||
@@ -2221,14 +2362,30 @@ public class RecipientDatabase extends Database {
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2
|
||||
* migration.
|
||||
*/
|
||||
void updateGroupId(@NonNull GroupId.V1 v1Id, @NonNull GroupId.V2 v2Id) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(GROUP_ID, v2Id.toString());
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
|
||||
SqlUtil.Query query = SqlUtil.buildTrueUpdateQuery(GROUP_ID + " = ?", SqlUtil.buildArgs(v1Id), values);
|
||||
|
||||
if (update(query, values)) {
|
||||
RecipientId id = getByGroupId(v2Id).get();
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will update the database with the content values you specified. It will make an intelligent
|
||||
* query such that this will only return true if a row was *actually* updated.
|
||||
*/
|
||||
private boolean update(@NonNull RecipientId id, @NonNull ContentValues contentValues) {
|
||||
String selection = ID + " = ?";
|
||||
String[] args = new String[]{id.serialize()};
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues);
|
||||
SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(id), contentValues);
|
||||
|
||||
return update(updateQuery, contentValues);
|
||||
}
|
||||
@@ -2238,7 +2395,7 @@ public class RecipientDatabase extends Database {
|
||||
* <p>
|
||||
* This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}.
|
||||
*/
|
||||
private boolean update(@NonNull SqlUtil.UpdateQuery updateQuery, @NonNull ContentValues contentValues) {
|
||||
private boolean update(@NonNull SqlUtil.Query updateQuery, @NonNull ContentValues contentValues) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0;
|
||||
@@ -2333,7 +2490,7 @@ public class RecipientDatabase extends Database {
|
||||
uuidValues.put(SYSTEM_PHONE_LABEL, e164Settings.getSystemPhoneLabel());
|
||||
uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri());
|
||||
uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing());
|
||||
uuidValues.put(GROUPS_V2_CAPABILITY, uuidSettings.getGroupsV2Capability() != Recipient.Capability.UNKNOWN ? uuidSettings.getGroupsV2Capability().serialize() : e164Settings.getGroupsV2Capability().serialize());
|
||||
uuidValues.put(CAPABILITIES, Math.max(uuidSettings.getCapabilities(), e164Settings.getCapabilities()));
|
||||
uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.ALWAYS_NOTIFY ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId());
|
||||
if (uuidSettings.getProfileKey() != null) {
|
||||
updateProfileValuesForMerge(uuidValues, uuidSettings);
|
||||
@@ -2525,7 +2682,6 @@ public class RecipientDatabase extends Database {
|
||||
private final String e164;
|
||||
private final String email;
|
||||
private final GroupId groupId;
|
||||
private final GroupMasterKey groupMasterKey;
|
||||
private final GroupType groupType;
|
||||
private final boolean blocked;
|
||||
private final long muteUntil;
|
||||
@@ -2551,14 +2707,13 @@ public class RecipientDatabase extends Database {
|
||||
private final String notificationChannel;
|
||||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
private final Recipient.Capability uuidCapability;
|
||||
private final long capabilities;
|
||||
private final Recipient.Capability groupsV2Capability;
|
||||
private final Recipient.Capability groupsV1MigrationCapability;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageId;
|
||||
private final byte[] identityKey;
|
||||
private final IdentityDatabase.VerifiedStatus identityStatus;
|
||||
private final MentionSetting mentionSetting;
|
||||
private final byte[] storageProto;
|
||||
private final SyncExtras syncExtras;
|
||||
|
||||
RecipientSettings(@NonNull RecipientId id,
|
||||
@Nullable UUID uuid,
|
||||
@@ -2566,7 +2721,6 @@ public class RecipientDatabase extends Database {
|
||||
@Nullable String e164,
|
||||
@Nullable String email,
|
||||
@Nullable GroupId groupId,
|
||||
@Nullable GroupMasterKey groupMasterKey,
|
||||
@NonNull GroupType groupType,
|
||||
boolean blocked,
|
||||
long muteUntil,
|
||||
@@ -2592,55 +2746,50 @@ public class RecipientDatabase extends Database {
|
||||
@Nullable String notificationChannel,
|
||||
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
|
||||
boolean forceSmsSelection,
|
||||
Recipient.Capability uuidCapability,
|
||||
Recipient.Capability groupsV2Capability,
|
||||
long capabilities,
|
||||
@NonNull InsightsBannerTier insightsBannerTier,
|
||||
@Nullable byte[] storageId,
|
||||
@Nullable byte[] identityKey,
|
||||
@NonNull IdentityDatabase.VerifiedStatus identityStatus,
|
||||
@NonNull MentionSetting mentionSetting,
|
||||
@Nullable byte[] storageProto)
|
||||
@NonNull SyncExtras syncExtras)
|
||||
{
|
||||
this.id = id;
|
||||
this.uuid = uuid;
|
||||
this.username = username;
|
||||
this.e164 = e164;
|
||||
this.email = email;
|
||||
this.groupId = groupId;
|
||||
this.groupMasterKey = groupMasterKey;
|
||||
this.groupType = groupType;
|
||||
this.blocked = blocked;
|
||||
this.muteUntil = muteUntil;
|
||||
this.messageVibrateState = messageVibrateState;
|
||||
this.callVibrateState = callVibrateState;
|
||||
this.messageRingtone = messageRingtone;
|
||||
this.callRingtone = callRingtone;
|
||||
this.color = color;
|
||||
this.defaultSubscriptionId = defaultSubscriptionId;
|
||||
this.expireMessages = expireMessages;
|
||||
this.registered = registered;
|
||||
this.profileKey = profileKey;
|
||||
this.profileKeyCredential = profileKeyCredential;
|
||||
this.systemDisplayName = systemDisplayName;
|
||||
this.systemContactPhoto = systemContactPhoto;
|
||||
this.systemPhoneLabel = systemPhoneLabel;
|
||||
this.systemContactUri = systemContactUri;
|
||||
this.signalProfileName = signalProfileName;
|
||||
this.signalProfileAvatar = signalProfileAvatar;
|
||||
this.hasProfileImage = hasProfileImage;
|
||||
this.profileSharing = profileSharing;
|
||||
this.lastProfileFetch = lastProfileFetch;
|
||||
this.notificationChannel = notificationChannel;
|
||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||
this.forceSmsSelection = forceSmsSelection;
|
||||
this.uuidCapability = uuidCapability;
|
||||
this.groupsV2Capability = groupsV2Capability;
|
||||
this.insightsBannerTier = insightsBannerTier;
|
||||
this.storageId = storageId;
|
||||
this.identityKey = identityKey;
|
||||
this.identityStatus = identityStatus;
|
||||
this.mentionSetting = mentionSetting;
|
||||
this.storageProto = storageProto;
|
||||
this.id = id;
|
||||
this.uuid = uuid;
|
||||
this.username = username;
|
||||
this.e164 = e164;
|
||||
this.email = email;
|
||||
this.groupId = groupId;
|
||||
this.groupType = groupType;
|
||||
this.blocked = blocked;
|
||||
this.muteUntil = muteUntil;
|
||||
this.messageVibrateState = messageVibrateState;
|
||||
this.callVibrateState = callVibrateState;
|
||||
this.messageRingtone = messageRingtone;
|
||||
this.callRingtone = callRingtone;
|
||||
this.color = color;
|
||||
this.defaultSubscriptionId = defaultSubscriptionId;
|
||||
this.expireMessages = expireMessages;
|
||||
this.registered = registered;
|
||||
this.profileKey = profileKey;
|
||||
this.profileKeyCredential = profileKeyCredential;
|
||||
this.systemDisplayName = systemDisplayName;
|
||||
this.systemContactPhoto = systemContactPhoto;
|
||||
this.systemPhoneLabel = systemPhoneLabel;
|
||||
this.systemContactUri = systemContactUri;
|
||||
this.signalProfileName = signalProfileName;
|
||||
this.signalProfileAvatar = signalProfileAvatar;
|
||||
this.hasProfileImage = hasProfileImage;
|
||||
this.profileSharing = profileSharing;
|
||||
this.lastProfileFetch = lastProfileFetch;
|
||||
this.notificationChannel = notificationChannel;
|
||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||
this.forceSmsSelection = forceSmsSelection;
|
||||
this.capabilities = capabilities;
|
||||
this.groupsV2Capability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH));
|
||||
this.groupsV1MigrationCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH));
|
||||
this.insightsBannerTier = insightsBannerTier;
|
||||
this.storageId = storageId;
|
||||
this.mentionSetting = mentionSetting;
|
||||
this.syncExtras = syncExtras;
|
||||
}
|
||||
|
||||
public RecipientId getId() {
|
||||
@@ -2667,13 +2816,6 @@ public class RecipientDatabase extends Database {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only read populated for sync.
|
||||
*/
|
||||
public @Nullable GroupMasterKey getGroupMasterKey() {
|
||||
return groupMasterKey;
|
||||
}
|
||||
|
||||
public @NonNull GroupType getGroupType() {
|
||||
return groupType;
|
||||
}
|
||||
@@ -2778,32 +2920,80 @@ public class RecipientDatabase extends Database {
|
||||
return forceSmsSelection;
|
||||
}
|
||||
|
||||
public Recipient.Capability getUuidCapability() {
|
||||
return uuidCapability;
|
||||
public @NonNull Recipient.Capability getGroupsV2Capability() {
|
||||
return groupsV2Capability;
|
||||
}
|
||||
|
||||
public Recipient.Capability getGroupsV2Capability() {
|
||||
return groupsV2Capability;
|
||||
public @NonNull Recipient.Capability getGroupsV1MigrationCapability() {
|
||||
return groupsV1MigrationCapability;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageId() {
|
||||
return storageId;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public @NonNull IdentityDatabase.VerifiedStatus getIdentityStatus() {
|
||||
return identityStatus;
|
||||
}
|
||||
|
||||
public @NonNull MentionSetting getMentionSetting() {
|
||||
return mentionSetting;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageProto() {
|
||||
return storageProto;
|
||||
public @NonNull SyncExtras getSyncExtras() {
|
||||
return syncExtras;
|
||||
}
|
||||
|
||||
long getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bundle of data that's only necessary when syncing to storage service, not for a
|
||||
* {@link Recipient}.
|
||||
*/
|
||||
public static class SyncExtras {
|
||||
private final byte[] storageProto;
|
||||
private final GroupMasterKey groupMasterKey;
|
||||
private final byte[] identityKey;
|
||||
private final VerifiedStatus identityStatus;
|
||||
private final boolean archived;
|
||||
private final boolean forcedUnread;
|
||||
|
||||
public SyncExtras(@Nullable byte[] storageProto,
|
||||
@Nullable GroupMasterKey groupMasterKey,
|
||||
@Nullable byte[] identityKey,
|
||||
@NonNull VerifiedStatus identityStatus,
|
||||
boolean archived,
|
||||
boolean forcedUnread)
|
||||
{
|
||||
this.storageProto = storageProto;
|
||||
this.groupMasterKey = groupMasterKey;
|
||||
this.identityKey = identityKey;
|
||||
this.identityStatus = identityStatus;
|
||||
this.archived = archived;
|
||||
this.forcedUnread = forcedUnread;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageProto() {
|
||||
return storageProto;
|
||||
}
|
||||
|
||||
public @Nullable GroupMasterKey getGroupMasterKey() {
|
||||
return groupMasterKey;
|
||||
}
|
||||
|
||||
public boolean isArchived() {
|
||||
return archived;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public @NonNull VerifiedStatus getIdentityStatus() {
|
||||
return identityStatus;
|
||||
}
|
||||
|
||||
public boolean isForcedUnread() {
|
||||
return forcedUnread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,8 +58,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
@@ -362,8 +364,8 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsMissedCall(long id) {
|
||||
updateTypeBitmask(id, Types.TOTAL_MASK, Types.MISSED_CALL_TYPE);
|
||||
public void markAsMissedCall(long id, boolean isVideoOffer) {
|
||||
updateTypeBitmask(id, Types.TOTAL_MASK, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -373,6 +375,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(REMOTE_DELETED, 1);
|
||||
values.putNull(BODY);
|
||||
values.putNull(REACTIONS);
|
||||
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) });
|
||||
|
||||
long threadId = getThreadIdForMessage(id);
|
||||
@@ -630,12 +633,14 @@ public class SmsDatabase extends MessageDatabase {
|
||||
@Override
|
||||
public boolean hasReceivedAnyCallsSince(long threadId, long timestamp) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = new String[]{SmsDatabase.TYPE};
|
||||
String selection = THREAD_ID + " = ? AND " + DATE_RECEIVED + " > ? AND (" + TYPE + " = ? OR " + TYPE + " = ?)";
|
||||
String[] selectionArgs = new String[]{String.valueOf(threadId),
|
||||
String.valueOf(timestamp),
|
||||
String.valueOf(Types.INCOMING_CALL_TYPE),
|
||||
String.valueOf(Types.MISSED_CALL_TYPE)};
|
||||
String[] projection = SqlUtil.buildArgs(SmsDatabase.TYPE);
|
||||
String selection = THREAD_ID + " = ? AND " + DATE_RECEIVED + " > ? AND (" + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " =?)";
|
||||
String[] selectionArgs = SqlUtil.buildArgs(threadId,
|
||||
timestamp,
|
||||
Types.INCOMING_AUDIO_CALL_TYPE,
|
||||
Types.INCOMING_VIDEO_CALL_TYPE,
|
||||
Types.MISSED_AUDIO_CALL_TYPE,
|
||||
Types.MISSED_VIDEO_CALL_TYPE);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, projection, selection, selectionArgs, null, null, null)) {
|
||||
return cursor != null && cursor.moveToFirst();
|
||||
@@ -643,18 +648,18 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address) {
|
||||
return insertCallLog(address, Types.INCOMING_CALL_TYPE, false, System.currentTimeMillis());
|
||||
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) {
|
||||
return insertCallLog(address, isVideoOffer ? Types.INCOMING_VIDEO_CALL_TYPE : Types.INCOMING_AUDIO_CALL_TYPE, false, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address) {
|
||||
return insertCallLog(address, Types.OUTGOING_CALL_TYPE, false, System.currentTimeMillis());
|
||||
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer) {
|
||||
return insertCallLog(address, isVideoOffer ? Types.OUTGOING_VIDEO_CALL_TYPE : Types.OUTGOING_AUDIO_CALL_TYPE, false, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp) {
|
||||
return insertCallLog(address, Types.MISSED_CALL_TYPE, true, timestamp);
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer) {
|
||||
return insertCallLog(address, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE, true, timestamp);
|
||||
}
|
||||
|
||||
private @NonNull Pair<Long, Long> insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) {
|
||||
@@ -684,6 +689,21 @@ public class SmsDatabase extends MessageDatabase {
|
||||
return new Pair<>(messageId, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp) {
|
||||
String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " >= ? AND " + TYPE + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(threadId, afterTimestamp, Types.PROFILE_CHANGE_TYPE);
|
||||
|
||||
try (Reader reader = readerFor(queryMessages(where, args, true, -1))) {
|
||||
List<MessageRecord> results = new ArrayList<>(reader.getCount());
|
||||
while (reader.getNext() != null) {
|
||||
results.add(reader.getCurrent());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
@@ -735,6 +755,39 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, @NonNull List<RecipientId> pendingRecipients) {
|
||||
insertGroupV1MigrationNotification(recipientId, threadId);
|
||||
|
||||
if (pendingRecipients.size() > 0) {
|
||||
insertGroupV1MigrationEvent(recipientId, threadId, pendingRecipients);
|
||||
}
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
|
||||
}
|
||||
|
||||
private void insertGroupV1MigrationNotification(@NonNull RecipientId recipientId, long threadId) {
|
||||
insertGroupV1MigrationEvent(recipientId, threadId, Collections.emptyList());
|
||||
}
|
||||
|
||||
private void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, @NonNull List<RecipientId> pendingRecipients) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(RECIPIENT_ID, recipientId.serialize());
|
||||
values.put(ADDRESS_DEVICE_ID, 1);
|
||||
values.put(DATE_RECEIVED, System.currentTimeMillis());
|
||||
values.put(DATE_SENT, System.currentTimeMillis());
|
||||
values.put(READ, 1);
|
||||
values.put(TYPE, Types.GV1_MIGRATION_TYPE);
|
||||
values.put(THREAD_ID, threadId);
|
||||
|
||||
if (pendingRecipients.size() > 0) {
|
||||
values.put(BODY, RecipientId.toSerializedList(pendingRecipients));
|
||||
}
|
||||
|
||||
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
|
||||
if (message.isJoined()) {
|
||||
@@ -771,7 +824,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
if (message.getGroupId() == null) {
|
||||
groupRecipient = null;
|
||||
} else {
|
||||
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(message.getGroupId());
|
||||
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(message.getGroupId());
|
||||
groupRecipient = Recipient.resolved(id);
|
||||
}
|
||||
|
||||
@@ -926,6 +979,8 @@ public class SmsDatabase extends MessageDatabase {
|
||||
|
||||
@Override
|
||||
public boolean deleteMessage(long messageId) {
|
||||
Log.d(TAG, "deleteMessage(" + messageId + ")");
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
|
||||
@@ -962,6 +1017,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
|
||||
@Override
|
||||
void deleteThread(long threadId) {
|
||||
Log.d(TAG, "deleteThread(" + threadId + ")");
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
|
||||
}
|
||||
@@ -982,8 +1038,40 @@ public class SmsDatabase extends MessageDatabase {
|
||||
db.delete(TABLE_NAME, where, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) {
|
||||
String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " +
|
||||
TABLE_NAME + "." + getDateReceivedColumnName() + " >= ?";
|
||||
String[] args = SqlUtil.buildArgs(threadId, timestamp);
|
||||
|
||||
try (Reader reader = readerFor(queryMessages(where, args, false, limit))) {
|
||||
List<MessageRecord> results = new ArrayList<>(reader.cursor.getCount());
|
||||
|
||||
while (reader.getNext() != null) {
|
||||
results.add(reader.getCurrent());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
private Cursor queryMessages(@NonNull String where, @NonNull String[] args, boolean reverse, long limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
return db.query(TABLE_NAME,
|
||||
MESSAGE_PROJECTION,
|
||||
where,
|
||||
args,
|
||||
null,
|
||||
null,
|
||||
reverse ? ID + " DESC" : null,
|
||||
limit > 0 ? String.valueOf(limit) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
void deleteThreads(@NonNull Set<Long> threadIds) {
|
||||
Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")");
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = "";
|
||||
|
||||
@@ -998,6 +1086,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
|
||||
@Override
|
||||
void deleteAllThreads() {
|
||||
Log.d(TAG, "deleteAllThreads()");
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
@@ -1184,7 +1273,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Reader {
|
||||
public static class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
private final Context context;
|
||||
@@ -1255,6 +1344,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
@@ -41,6 +41,8 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
@@ -56,11 +58,14 @@ import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -70,7 +75,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ThreadDatabase extends Database {
|
||||
|
||||
@@ -102,7 +106,7 @@ public class ThreadDatabase extends Database {
|
||||
public static final String LAST_SEEN = "last_seen";
|
||||
public static final String HAS_SENT = "has_sent";
|
||||
private static final String LAST_SCROLLED = "last_scrolled";
|
||||
private static final String PINNED = "pinned";
|
||||
static final String PINNED = "pinned";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
DATE + " INTEGER DEFAULT 0, " +
|
||||
@@ -144,7 +148,7 @@ public class ThreadDatabase extends Database {
|
||||
.toList();
|
||||
|
||||
private static final List<String> COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION = Stream.concat(Stream.concat(Stream.of(TYPED_THREAD_PROJECTION),
|
||||
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION)),
|
||||
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION_NO_ID)),
|
||||
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
|
||||
.toList();
|
||||
|
||||
@@ -270,6 +274,7 @@ public class ThreadDatabase extends Database {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] { ID }, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
@@ -283,6 +288,7 @@ public class ThreadDatabase extends Database {
|
||||
mmsSmsDatabase.deleteAbandonedMessages();
|
||||
attachmentDatabase.trimAllAbandonedAttachments();
|
||||
groupReceiptDatabase.deleteAbandonedRows();
|
||||
mentionDatabase.deleteAbandonedMentions();
|
||||
attachmentDatabase.deleteAbandonedAttachmentFiles();
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
@@ -304,6 +310,7 @@ public class ThreadDatabase extends Database {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
@@ -312,6 +319,7 @@ public class ThreadDatabase extends Database {
|
||||
mmsSmsDatabase.deleteAbandonedMessages();
|
||||
attachmentDatabase.trimAllAbandonedAttachments();
|
||||
groupReceiptDatabase.deleteAbandonedRows();
|
||||
mentionDatabase.deleteAbandonedMentions();
|
||||
attachmentDatabase.deleteAbandonedAttachmentFiles();
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
@@ -401,6 +409,7 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
List<MarkedMessageInfo> smsRecords = new LinkedList<>();
|
||||
List<MarkedMessageInfo> mmsRecords = new LinkedList<>();
|
||||
boolean needsSync = false;
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
@@ -413,6 +422,8 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
for (long threadId : threadIds) {
|
||||
ThreadRecord previous = getThreadRecord(threadId);
|
||||
|
||||
smsRecords.addAll(DatabaseFactory.getSmsDatabase(context).setMessagesReadSince(threadId, sinceTimestamp));
|
||||
mmsRecords.addAll(DatabaseFactory.getMmsDatabase(context).setMessagesReadSince(threadId, sinceTimestamp));
|
||||
|
||||
@@ -423,7 +434,12 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
contentValues.put(UNREAD_COUNT, unreadCount);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{threadId + ""});
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId));
|
||||
|
||||
if (previous != null && previous.isForcedUnread()) {
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(previous.getRecipient().getId());
|
||||
needsSync = true;
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
@@ -433,6 +449,11 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
notifyConversationListeners(new HashSet<>(threadIds));
|
||||
notifyConversationListListeners();
|
||||
|
||||
if (needsSync) {
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
return Util.concatenatedList(smsRecords, mmsRecords);
|
||||
}
|
||||
|
||||
@@ -441,19 +462,22 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
List<RecipientId> recipientIds = getRecipientIdsForThreadIds(threadIds);
|
||||
SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, threadIds);
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
||||
contentValues.put(READ, ReadStatus.FORCED_UNREAD.serialize());
|
||||
|
||||
for (long threadId : threadIds) {
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] { String.valueOf(threadId) });
|
||||
}
|
||||
db.update(TABLE_NAME, contentValues, query.getWhere(), query.getWhereArgs());
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientIds);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListListeners();
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -552,6 +576,28 @@ public class ThreadDatabase extends Database {
|
||||
return db.rawQuery(query, null);
|
||||
}
|
||||
|
||||
public @NonNull List<ThreadRecord> getRecentV1Groups(int limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = MESSAGE_COUNT + " != 0 AND " +
|
||||
"(" +
|
||||
GroupDatabase.TABLE_NAME + "." + GroupDatabase.ACTIVE + " = 1 AND " +
|
||||
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY + " IS NULL AND " +
|
||||
GroupDatabase.TABLE_NAME + "." + GroupDatabase.MMS + " = 0" +
|
||||
")";
|
||||
String query = createQuery(where, 0, limit, true);
|
||||
|
||||
List<ThreadRecord> threadRecords = new ArrayList<>();
|
||||
|
||||
try (Reader reader = readerFor(db.rawQuery(query, null))) {
|
||||
ThreadRecord record;
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
threadRecords.add(record);
|
||||
}
|
||||
}
|
||||
return threadRecords;
|
||||
}
|
||||
|
||||
public Cursor getConversationList() {
|
||||
return getConversationList("0");
|
||||
}
|
||||
@@ -672,7 +718,7 @@ public class ThreadDatabase extends Database {
|
||||
final String query;
|
||||
|
||||
if (pinned) {
|
||||
query = createQuery(where, PINNED + " ASC", offset, limit, false);
|
||||
query = createQuery(where, PINNED + " ASC", offset, limit);
|
||||
} else {
|
||||
query = createQuery(where, offset, limit, false);
|
||||
}
|
||||
@@ -737,6 +783,25 @@ public class ThreadDatabase extends Database {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Pinned recipients, in order from top to bottom.
|
||||
*/
|
||||
public @NonNull List<RecipientId> getPinnedRecipientIds() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = new String[]{RECIPIENT_ID};
|
||||
String query = PINNED + " > ?";
|
||||
String[] args = SqlUtil.buildArgs(0);
|
||||
List<RecipientId> pinned = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, PINNED + " ASC")) {
|
||||
while (cursor.moveToNext()) {
|
||||
pinned.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)));
|
||||
}
|
||||
}
|
||||
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public void pinConversations(@NonNull Set<Long> threadIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
@@ -750,6 +815,7 @@ public class ThreadDatabase extends Database {
|
||||
contentValues.put(PINNED, ++pinnedCount);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId));
|
||||
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
@@ -759,6 +825,9 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
notifyConversationListListeners();
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
public void unpinConversations(@NonNull Set<Long> threadIds) {
|
||||
@@ -771,6 +840,9 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
db.update(TABLE_NAME, contentValues, selection, SqlUtil.buildArgs(Stream.of(threadIds).toArray()));
|
||||
notifyConversationListListeners();
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
public void archiveConversation(long threadId) {
|
||||
@@ -945,6 +1017,20 @@ public class ThreadDatabase extends Database {
|
||||
return Recipient.resolved(id);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> getRecipientIdsForThreadIds(Collection<Long> threadIds) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, threadIds);
|
||||
List<RecipientId> ids = new ArrayList<>(threadIds.size());
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { RECIPIENT_ID }, query.getWhere(), query.getWhereArgs(), null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)));
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public boolean hasThread(@NonNull RecipientId recipientId) {
|
||||
return getThreadIdIfExistsFor(recipientId) > -1;
|
||||
}
|
||||
@@ -960,16 +1046,106 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
void updateReadState(long threadId) {
|
||||
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
|
||||
ThreadRecord previous = getThreadRecord(threadId);
|
||||
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(READ, unreadCount == 0);
|
||||
contentValues.put(READ, unreadCount == 0 ? ReadStatus.READ.serialize() : ReadStatus.UNREAD.serialize());
|
||||
contentValues.put(UNREAD_COUNT, unreadCount);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,ID_WHERE,
|
||||
new String[] {String.valueOf(threadId)});
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId));
|
||||
|
||||
notifyConversationListListeners();
|
||||
|
||||
if (previous != null && previous.isForcedUnread()) {
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(previous.getRecipient().getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalContactRecord record) {
|
||||
applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread());
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalGroupV1Record record) {
|
||||
applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread());
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalGroupV2Record record) {
|
||||
applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread());
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalAccountRecord record) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived(), record.isNoteToSelfForcedUnread());
|
||||
|
||||
ContentValues clearPinnedValues = new ContentValues();
|
||||
clearPinnedValues.put(PINNED, 0);
|
||||
db.update(TABLE_NAME, clearPinnedValues, null, null);
|
||||
|
||||
int pinnedPosition = 1;
|
||||
for (SignalAccountRecord.PinnedConversation pinned : record.getPinnedConversations()) {
|
||||
ContentValues pinnedValues = new ContentValues();
|
||||
pinnedValues.put(PINNED, pinnedPosition);
|
||||
|
||||
Recipient pinnedRecipient;
|
||||
|
||||
if (pinned.getContact().isPresent()) {
|
||||
pinnedRecipient = Recipient.externalPush(context, pinned.getContact().get());
|
||||
} else if (pinned.getGroupV1Id().isPresent()) {
|
||||
try {
|
||||
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
|
||||
} catch (BadGroupIdException e) {
|
||||
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e);
|
||||
pinnedRecipient = null;
|
||||
}
|
||||
} else if (pinned.getGroupV2MasterKey().isPresent()) {
|
||||
try {
|
||||
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e);
|
||||
pinnedRecipient = null;
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Empty pinned conversation on the AccountRecord?");
|
||||
pinnedRecipient = null;
|
||||
}
|
||||
|
||||
if (pinnedRecipient != null) {
|
||||
db.update(TABLE_NAME, pinnedValues, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(pinnedRecipient.getId()));
|
||||
}
|
||||
|
||||
pinnedPosition++;
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
private void applyStorageSyncUpdate(@NonNull RecipientId recipientId, boolean archived, boolean forcedUnread) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ARCHIVED, archived);
|
||||
|
||||
if (forcedUnread) {
|
||||
values.put(READ, ReadStatus.FORCED_UNREAD.serialize());
|
||||
} else {
|
||||
Long threadId = getThreadIdFor(recipientId);
|
||||
if (threadId != null) {
|
||||
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
|
||||
|
||||
values.put(READ, unreadCount == 0 ? ReadStatus.READ.serialize() : ReadStatus.UNREAD.serialize());
|
||||
values.put(UNREAD_COUNT, unreadCount);
|
||||
}
|
||||
}
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(recipientId));
|
||||
}
|
||||
|
||||
public boolean update(long threadId, boolean unarchive) {
|
||||
@@ -1145,10 +1321,10 @@ public class ThreadDatabase extends Database {
|
||||
return Extra.forMessageRequest();
|
||||
}
|
||||
|
||||
if (record.isViewOnce()) {
|
||||
return Extra.forViewOnce();
|
||||
} else if (record.isRemoteDelete()) {
|
||||
if (record.isRemoteDelete()) {
|
||||
return Extra.forRemoteDelete();
|
||||
} else if (record.isViewOnce()) {
|
||||
return Extra.forViewOnce();
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
|
||||
StickerSlide slide = Objects.requireNonNull(((MmsMessageRecord) record).getSlideDeck().getStickerSlide());
|
||||
return Extra.forSticker(slide.getEmoji());
|
||||
@@ -1166,10 +1342,10 @@ public class ThreadDatabase extends Database {
|
||||
private @NonNull String createQuery(@NonNull String where, long offset, long limit, boolean preferPinned) {
|
||||
String orderBy = (preferPinned ? TABLE_NAME + "." + PINNED + " DESC, " : "") + TABLE_NAME + "." + DATE + " DESC";
|
||||
|
||||
return createQuery(where, orderBy, offset, limit, preferPinned);
|
||||
return createQuery(where, orderBy, offset, limit);
|
||||
}
|
||||
|
||||
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit, boolean preferPinned) {
|
||||
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit) {
|
||||
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
|
||||
|
||||
String query =
|
||||
@@ -1400,7 +1576,7 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
private enum ReadStatus {
|
||||
enum ReadStatus {
|
||||
READ(1), UNREAD(0), FORCED_UNREAD(2);
|
||||
|
||||
private final int value;
|
||||
|
||||
@@ -62,13 +62,18 @@ import org.thoughtcrime.securesms.util.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Triple;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@@ -149,8 +154,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int STICKER_EMOJI_IN_NOTIFICATIONS = 73;
|
||||
private static final int THUMBNAIL_CLEANUP = 74;
|
||||
private static final int STICKER_CONTENT_TYPE_CLEANUP = 75;
|
||||
private static final int MENTION_CLEANUP = 76;
|
||||
private static final int MENTION_CLEANUP_V2 = 77;
|
||||
private static final int REACTION_CLEANUP = 78;
|
||||
private static final int CAPABILITIES_REFACTOR = 79;
|
||||
private static final int GV1_MIGRATION = 80;
|
||||
|
||||
private static final int DATABASE_VERSION = 75;
|
||||
private static final int DATABASE_VERSION = 80;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@@ -1060,6 +1070,96 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
Log.i(TAG, "Updated " + rows + " sticker attachment content types.");
|
||||
}
|
||||
|
||||
if (oldVersion < MENTION_CLEANUP) {
|
||||
String selectMentionIdsNotInGroupsV2 = "select mention._id from mention left join thread on mention.thread_id = thread._id left join recipient on thread.recipient_ids = recipient._id where recipient.group_type != 3";
|
||||
db.delete("mention", "_id in (" + selectMentionIdsNotInGroupsV2 + ")", null);
|
||||
db.delete("mention", "message_id NOT IN (SELECT _id FROM mms) OR thread_id NOT IN (SELECT _id from thread)", null);
|
||||
|
||||
List<Long> idsToDelete = new LinkedList<>();
|
||||
try (Cursor cursor = db.rawQuery("select mention.*, mms.body from mention inner join mms on mention.message_id = mms._id", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
int rangeStart = CursorUtil.requireInt(cursor, "range_start");
|
||||
int rangeLength = CursorUtil.requireInt(cursor, "range_length");
|
||||
String body = CursorUtil.requireString(cursor, "body");
|
||||
|
||||
if (body == null || body.isEmpty() || rangeStart < 0 || rangeLength < 0 || (rangeStart + rangeLength) > body.length()) {
|
||||
idsToDelete.add(CursorUtil.requireLong(cursor, "_id"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Util.hasItems(idsToDelete)) {
|
||||
String ids = TextUtils.join(",", idsToDelete);
|
||||
db.delete("mention", "_id in (" + ids + ")", null);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < MENTION_CLEANUP_V2) {
|
||||
String selectMentionIdsWithMismatchingThreadIds = "select mention._id from mention left join mms on mention.message_id = mms._id where mention.thread_id != mms.thread_id";
|
||||
db.delete("mention", "_id in (" + selectMentionIdsWithMismatchingThreadIds + ")", null);
|
||||
|
||||
List<Long> idsToDelete = new LinkedList<>();
|
||||
Set<Triple<Long, Integer, Integer>> mentionTuples = new HashSet<>();
|
||||
try (Cursor cursor = db.rawQuery("select mention.*, mms.body from mention inner join mms on mention.message_id = mms._id order by mention._id desc", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long mentionId = CursorUtil.requireLong(cursor, "_id");
|
||||
long messageId = CursorUtil.requireLong(cursor, "message_id");
|
||||
int rangeStart = CursorUtil.requireInt(cursor, "range_start");
|
||||
int rangeLength = CursorUtil.requireInt(cursor, "range_length");
|
||||
String body = CursorUtil.requireString(cursor, "body");
|
||||
|
||||
if (body != null && rangeStart < body.length() && body.charAt(rangeStart) != '\uFFFC') {
|
||||
idsToDelete.add(mentionId);
|
||||
} else {
|
||||
Triple<Long, Integer, Integer> tuple = new Triple<>(messageId, rangeStart, rangeLength);
|
||||
if (mentionTuples.contains(tuple)) {
|
||||
idsToDelete.add(mentionId);
|
||||
} else {
|
||||
mentionTuples.add(tuple);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Util.hasItems(idsToDelete)) {
|
||||
String ids = TextUtils.join(",", idsToDelete);
|
||||
db.delete("mention", "_id in (" + ids + ")", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < REACTION_CLEANUP) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull("reactions");
|
||||
db.update("sms", values, "remote_deleted = ?", new String[] { "1" });
|
||||
}
|
||||
|
||||
if (oldVersion < CAPABILITIES_REFACTOR) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN capabilities INTEGER DEFAULT 0");
|
||||
|
||||
db.execSQL("UPDATE recipient SET capabilities = 1 WHERE gv2_capability = 1");
|
||||
db.execSQL("UPDATE recipient SET capabilities = 2 WHERE gv2_capability = -1");
|
||||
}
|
||||
|
||||
if (oldVersion < GV1_MIGRATION) {
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN expected_v2_id TEXT DEFAULT NULL");
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN former_v1_members TEXT DEFAULT NULL");
|
||||
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON groups (expected_v2_id)");
|
||||
|
||||
int count = 0;
|
||||
try (Cursor cursor = db.rawQuery("SELECT * FROM groups WHERE group_id LIKE '__textsecure_group__!%' AND LENGTH(group_id) = 53", null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
String gv1 = CursorUtil.requireString(cursor, "group_id");
|
||||
String gv2 = GroupId.parseOrThrow(gv1).requireV1().deriveV2MigrationGroupId().toString();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("expected_v2_id", gv2);
|
||||
count += db.update("groups", values, "group_id = ?", SqlUtil.buildArgs(gv1));
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Updated " + count + " GV1 groups with expected GV2 IDs.");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
|
||||
@@ -15,7 +16,7 @@ public class RecentPhotosLoader extends CursorLoader {
|
||||
public static Uri BASE_URL = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
MediaStore.Images.ImageColumns.DATA,
|
||||
MediaStore.Images.ImageColumns._ID,
|
||||
MediaStore.Images.ImageColumns.DATE_TAKEN,
|
||||
MediaStore.Images.ImageColumns.DATE_MODIFIED,
|
||||
MediaStore.Images.ImageColumns.ORIENTATION,
|
||||
@@ -26,7 +27,8 @@ public class RecentPhotosLoader extends CursorLoader {
|
||||
MediaStore.Images.ImageColumns.HEIGHT
|
||||
};
|
||||
|
||||
private static final String SELECTION = MediaStore.Images.Media.DATA + " NOT NULL";
|
||||
private static final String SELECTION = Build.VERSION.SDK_INT > 28 ? MediaStore.Images.Media.IS_PENDING + " != 1"
|
||||
: MediaStore.Images.Media.DATA + " IS NULL";
|
||||
|
||||
private final Context context;
|
||||
|
||||
|
||||
@@ -140,16 +140,28 @@ public abstract class DisplayRecord {
|
||||
return SmsDatabase.Types.isJoinedType(type);
|
||||
}
|
||||
|
||||
public boolean isIncomingCall() {
|
||||
return SmsDatabase.Types.isIncomingCall(type);
|
||||
public boolean isIncomingAudioCall() {
|
||||
return SmsDatabase.Types.isIncomingAudioCall(type);
|
||||
}
|
||||
|
||||
public boolean isOutgoingCall() {
|
||||
return SmsDatabase.Types.isOutgoingCall(type);
|
||||
public boolean isIncomingVideoCall() {
|
||||
return SmsDatabase.Types.isIncomingVideoCall(type);
|
||||
}
|
||||
|
||||
public boolean isMissedCall() {
|
||||
return SmsDatabase.Types.isMissedCall(type);
|
||||
public boolean isOutgoingAudioCall() {
|
||||
return SmsDatabase.Types.isOutgoingAudioCall(type);
|
||||
}
|
||||
|
||||
public boolean isOutgoingVideoCall() {
|
||||
return SmsDatabase.Types.isOutgoingVideoCall(type);
|
||||
}
|
||||
|
||||
public final boolean isMissedAudioCall() {
|
||||
return SmsDatabase.Types.isMissedAudioCall(type);
|
||||
}
|
||||
|
||||
public final boolean isMissedVideoCall() {
|
||||
return SmsDatabase.Types.isMissedVideoCall(type);
|
||||
}
|
||||
|
||||
public boolean isVerificationStatusChange() {
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
@@ -62,26 +64,30 @@ final class GroupsV2UpdateMessageProducer {
|
||||
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange decryptedGroupChange) {
|
||||
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
|
||||
if (selfPending.isPresent()) {
|
||||
return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy));
|
||||
return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
|
||||
}
|
||||
|
||||
ByteString foundingMemberUuid = decryptedGroupChange.getEditor();
|
||||
if (!foundingMemberUuid.isEmpty()) {
|
||||
if (selfUuidBytes.equals(foundingMemberUuid)) {
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group));
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
|
||||
} else {
|
||||
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator));
|
||||
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
|
||||
}
|
||||
}
|
||||
|
||||
if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid).isPresent()) {
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
|
||||
} else {
|
||||
return updateDescription(context.getString(R.string.MessageRecord_group_updated));
|
||||
return updateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
|
||||
}
|
||||
}
|
||||
|
||||
List<UpdateDescription> describeChanges(@NonNull DecryptedGroupChange change) {
|
||||
List<UpdateDescription> describeChanges(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change) {
|
||||
if (DecryptedGroup.getDefaultInstance().equals(previousGroupState)) {
|
||||
previousGroupState = null;
|
||||
}
|
||||
|
||||
List<UpdateDescription> updates = new LinkedList<>();
|
||||
|
||||
if (change.getEditor().isEmpty() || UuidUtil.UNKNOWN_UUID.equals(UuidUtil.fromByteString(change.getEditor()))) {
|
||||
@@ -96,7 +102,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
describeUnknownEditorNewTimer(change, updates);
|
||||
describeUnknownEditorNewAttributeAccess(change, updates);
|
||||
describeUnknownEditorNewMembershipAccess(change, updates);
|
||||
describeUnknownEditorNewGroupInviteLinkAccess(change, updates);
|
||||
describeUnknownEditorNewGroupInviteLinkAccess(previousGroupState, change, updates);
|
||||
describeRequestingMembers(change, updates);
|
||||
describeUnknownEditorRequestingMembersApprovals(change, updates);
|
||||
describeUnknownEditorRequestingMembersDeletes(change, updates);
|
||||
@@ -119,7 +125,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
describeNewTimer(change, updates);
|
||||
describeNewAttributeAccess(change, updates);
|
||||
describeNewMembershipAccess(change, updates);
|
||||
describeNewGroupInviteLinkAccess(change, updates);
|
||||
describeNewGroupInviteLinkAccess(previousGroupState, change, updates);
|
||||
describeRequestingMembers(change, updates);
|
||||
describeRequestingMembersApprovals(change, updates);
|
||||
describeRequestingMembersDeletes(change, updates);
|
||||
@@ -141,14 +147,14 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), (editor) -> context.getString(R.string.MessageRecord_s_updated_group, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), (editor) -> context.getString(R.string.MessageRecord_s_updated_group, editor), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorUnknownChange(@NonNull List<UpdateDescription> updates) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_was_updated)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_was_updated), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
|
||||
}
|
||||
|
||||
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
@@ -159,18 +165,18 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_group_link)));
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_group_link), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added)));
|
||||
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
} else {
|
||||
if (member.getUuid().equals(change.getEditor())) {
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_group_link, newMember)));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_group_link, newMember), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember)));
|
||||
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,9 +188,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember)));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,18 +203,18 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (editorIsYou) {
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(member, removedMember -> context.getString(R.string.MessageRecord_you_removed_s, removedMember)));
|
||||
updates.add(updateDescription(member, removedMember -> context.getString(R.string.MessageRecord_you_removed_s, removedMember), R.drawable.ic_update_group_remove_light_16, R.drawable.ic_update_group_remove_dark_16));
|
||||
}
|
||||
} else {
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_removed_you_from_the_group, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_removed_you_from_the_group, editor), R.drawable.ic_update_group_remove_light_16, R.drawable.ic_update_group_remove_dark_16));
|
||||
} else {
|
||||
if (member.equals(change.getEditor())) {
|
||||
updates.add(updateDescription(member, leavingMember -> context.getString(R.string.MessageRecord_s_left_the_group, leavingMember)));
|
||||
updates.add(updateDescription(member, leavingMember -> context.getString(R.string.MessageRecord_s_left_the_group, leavingMember), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), member, (editor, removedMember) -> context.getString(R.string.MessageRecord_s_removed_s, editor, removedMember)));
|
||||
updates.add(updateDescription(change.getEditor(), member, (editor, removedMember) -> context.getString(R.string.MessageRecord_s_removed_s, editor, removedMember), R.drawable.ic_update_group_remove_light_16, R.drawable.ic_update_group_remove_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,9 +226,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean removedMemberIsYou = member.equals(selfUuidBytes);
|
||||
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(member, oldMember -> context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, oldMember)));
|
||||
updates.add(updateDescription(member, oldMember -> context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, oldMember), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,23 +240,23 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
|
||||
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_you_made_s_an_admin, newAdmin)));
|
||||
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_you_made_s_an_admin, newAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_made_you_an_admin, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_made_you_an_admin, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, newAdmin) -> context.getString(R.string.MessageRecord_s_made_s_an_admin, editor, newAdmin)));
|
||||
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, newAdmin) -> context.getString(R.string.MessageRecord_s_made_s_an_admin, editor, newAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, oldAdmin)));
|
||||
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, oldAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, oldAdmin) -> context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, editor, oldAdmin)));
|
||||
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, oldAdmin) -> context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, editor, oldAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,15 +269,15 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_s_is_now_an_admin, newAdmin)));
|
||||
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_s_is_now_an_admin, newAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, oldAdmin)));
|
||||
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, oldAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,10 +291,10 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor)));
|
||||
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
} else {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(invitee.getUuid(), newInvitee -> context.getString(R.string.MessageRecord_you_invited_s_to_the_group, newInvitee)));
|
||||
updates.add(updateDescription(invitee.getUuid(), newInvitee -> context.getString(R.string.MessageRecord_you_invited_s_to_the_group, newInvitee), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
} else {
|
||||
notYouInviteCount++;
|
||||
}
|
||||
@@ -297,7 +303,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (notYouInviteCount > 0) {
|
||||
final int notYouInviteCountFinalCopy = notYouInviteCount;
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, editor, notYouInviteCountFinalCopy)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, editor, notYouInviteCountFinalCopy), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,9 +317,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
UUID uuid = UuidUtil.fromByteStringOrUnknown(invitee.getAddedByUuid());
|
||||
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group)));
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
} else {
|
||||
updates.add(0, updateDescription(invitee.getAddedByUuid(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor)));
|
||||
updates.add(0, updateDescription(invitee.getAddedByUuid(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
} else {
|
||||
notYouInviteCount++;
|
||||
@@ -321,7 +327,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
}
|
||||
|
||||
if (notYouInviteCount > 0) {
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount)));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,12 +339,12 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean decline = invitee.getUuid().equals(change.getEditor());
|
||||
if (decline) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
}
|
||||
} else if (invitee.getUuid().equals(selfUuidBytes)) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, editor), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
} else {
|
||||
notDeclineCount++;
|
||||
}
|
||||
@@ -346,10 +352,10 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (notDeclineCount > 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount)));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
} else {
|
||||
final int notDeclineCountFinalCopy = notDeclineCount;
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, editor, notDeclineCountFinalCopy)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, editor, notDeclineCountFinalCopy), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,14 +367,14 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean inviteeWasYou = invitee.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (inviteeWasYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
} else {
|
||||
notDeclineCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (notDeclineCount > 0) {
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount)));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,18 +387,18 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(uuid, newPromotedMember -> context.getString(R.string.MessageRecord_you_added_invited_member_s, newPromotedMember)));
|
||||
updates.add(updateDescription(uuid, newPromotedMember -> context.getString(R.string.MessageRecord_you_added_invited_member_s, newPromotedMember), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_light_16));
|
||||
} else {
|
||||
if (uuid.equals(change.getEditor())) {
|
||||
updates.add(updateDescription(uuid, newAcceptedMember -> context.getString(R.string.MessageRecord_s_accepted_invite, newAcceptedMember)));
|
||||
updates.add(updateDescription(uuid, newAcceptedMember -> context.getString(R.string.MessageRecord_s_accepted_invite, newAcceptedMember), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), uuid, (editor, newAcceptedMember) -> context.getString(R.string.MessageRecord_s_added_invited_member_s, editor, newAcceptedMember)));
|
||||
updates.add(updateDescription(change.getEditor(), uuid, (editor, newAcceptedMember) -> context.getString(R.string.MessageRecord_s_added_invited_member_s, editor, newAcceptedMember), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,9 +411,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(uuid, newMemberName -> context.getString(R.string.MessageRecord_s_joined_the_group, newMemberName)));
|
||||
updates.add(updateDescription(uuid, newMemberName -> context.getString(R.string.MessageRecord_s_joined_the_group, newMemberName), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,16 +424,16 @@ final class GroupsV2UpdateMessageProducer {
|
||||
if (change.hasNewTitle()) {
|
||||
String newTitle = StringUtil.isolateBidi(change.getNewTitle().getValue());
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle), R.drawable.ic_update_group_name_light_16, R.drawable.ic_update_group_name_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, editor, newTitle)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, editor, newTitle), R.drawable.ic_update_group_name_light_16, R.drawable.ic_update_group_name_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewTitle()) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.getNewTitle().getValue()))));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.getNewTitle().getValue())), R.drawable.ic_update_group_name_light_16, R.drawable.ic_update_group_name_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,16 +442,16 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (change.hasNewAvatar()) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar), R.drawable.ic_update_group_avatar_light_16, R.drawable.ic_update_group_avatar_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_avatar, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_avatar, editor), R.drawable.ic_update_group_avatar_light_16, R.drawable.ic_update_group_avatar_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewAvatar()) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed), R.drawable.ic_update_group_avatar_light_16, R.drawable.ic_update_group_avatar_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,9 +461,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
if (change.hasNewTimer()) {
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, editor, time)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, editor, time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,7 +471,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
private void describeUnknownEditorNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewTimer()) {
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,9 +481,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, editor, accessLevel)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, editor, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,7 +491,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
private void describeUnknownEditorNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,9 +501,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, editor, accessLevel)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, editor, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,11 +511,20 @@ final class GroupsV2UpdateMessageProducer {
|
||||
private void describeUnknownEditorNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
private void describeNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState,
|
||||
@NonNull DecryptedGroupChange change,
|
||||
@NonNull List<UpdateDescription> updates)
|
||||
{
|
||||
AccessControl.AccessRequired previousAccessControl = null;
|
||||
|
||||
if (previousGroupState != null) {
|
||||
previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink();
|
||||
}
|
||||
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
boolean groupLinkEnabled = false;
|
||||
|
||||
@@ -517,52 +532,85 @@ final class GroupsV2UpdateMessageProducer {
|
||||
case ANY:
|
||||
groupLinkEnabled = true;
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off)));
|
||||
if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_admin_approval_for_the_group_link), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor)));
|
||||
if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_admin_approval_for_the_group_link, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
groupLinkEnabled = true;
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on)));
|
||||
if (previousAccessControl == AccessControl.AccessRequired.ANY) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_admin_approval_for_the_group_link), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor)));
|
||||
if (previousAccessControl == AccessControl.AccessRequired.ANY) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_admin_approval_for_the_group_link, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UNSATISFIABLE:
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_group_link, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_group_link, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!groupLinkEnabled && change.getNewInviteLinkPassword().size() > 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_group_link)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_group_link), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_group_link, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_group_link, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
private void describeUnknownEditorNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState,
|
||||
@NonNull DecryptedGroupChange change,
|
||||
@NonNull List<UpdateDescription> updates)
|
||||
{
|
||||
AccessControl.AccessRequired previousAccessControl = null;
|
||||
|
||||
if (previousGroupState != null) {
|
||||
previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink();
|
||||
}
|
||||
|
||||
switch (change.getNewInviteLinkAccess()) {
|
||||
case ANY:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off)));
|
||||
if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_off), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on)));
|
||||
if (previousAccessControl == AccessControl.AccessRequired.ANY) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_on), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
break;
|
||||
case UNSATISFIABLE:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
break;
|
||||
}
|
||||
|
||||
if (change.getNewInviteLinkPassword().size() > 0) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_reset)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_reset), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,9 +619,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean requestingMemberIsYou = member.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting)));
|
||||
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -583,14 +631,14 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_approved_your_request_to_join_the_group, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_approved_your_request_to_join_the_group, editor), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
} else {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requesting)));
|
||||
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requesting), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), requestingMember.getUuid(), (editor, requesting) -> context.getString(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, editor, requesting)));
|
||||
updates.add(updateDescription(change.getEditor(), requestingMember.getUuid(), (editor, requesting) -> context.getString(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, editor, requesting), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -601,9 +649,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requesting)));
|
||||
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requesting), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -616,17 +664,17 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_canceled_your_request_to_join_the_group)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_canceled_your_request_to_join_the_group), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
}
|
||||
} else {
|
||||
boolean editorIsCanceledMember = change.getEditor().equals(requestingMember);
|
||||
|
||||
if (editorIsCanceledMember) {
|
||||
updates.add(updateDescription(requestingMember, editorRequesting -> context.getString(R.string.MessageRecord_s_canceled_their_request_to_join_the_group, editorRequesting)));
|
||||
updates.add(updateDescription(requestingMember, editorRequesting -> context.getString(R.string.MessageRecord_s_canceled_their_request_to_join_the_group, editorRequesting), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), requestingMember, (editor, requesting) -> context.getString(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, editor, requesting)));
|
||||
updates.add(updateDescription(change.getEditor(), requestingMember, (editor, requesting) -> context.getString(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, editor, requesting), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -637,9 +685,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
} else {
|
||||
updates.add(updateDescription(requestingMember, requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requesting)));
|
||||
updates.add(updateDescription(requestingMember, requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requesting), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -662,20 +710,32 @@ final class GroupsV2UpdateMessageProducer {
|
||||
String create(String arg1, String arg2);
|
||||
}
|
||||
|
||||
private static UpdateDescription updateDescription(@NonNull String string) {
|
||||
return UpdateDescription.staticDescription(string);
|
||||
private static UpdateDescription updateDescription(@NonNull String string,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource)
|
||||
{
|
||||
return UpdateDescription.staticDescription(string, lightIconResource, darkIconResource);
|
||||
}
|
||||
|
||||
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull StringFactory1Arg stringFactory) {
|
||||
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes,
|
||||
@NonNull StringFactory1Arg stringFactory,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource)
|
||||
{
|
||||
UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes);
|
||||
|
||||
return UpdateDescription.mentioning(Collections.singletonList(uuid1), () -> stringFactory.create(descriptionStrategy.describe(uuid1)));
|
||||
return UpdateDescription.mentioning(Collections.singletonList(uuid1), () -> stringFactory.create(descriptionStrategy.describe(uuid1)), lightIconResource, darkIconResource);
|
||||
}
|
||||
|
||||
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull ByteString uuid2Bytes, @NonNull StringFactory2Args stringFactory) {
|
||||
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes,
|
||||
@NonNull ByteString uuid2Bytes,
|
||||
@NonNull StringFactory2Args stringFactory,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource)
|
||||
{
|
||||
UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes);
|
||||
UUID uuid2 = UuidUtil.fromByteStringOrUnknown(uuid2Bytes);
|
||||
|
||||
return UpdateDescription.mentioning(Arrays.asList(uuid1, uuid2), () -> stringFactory.create(descriptionStrategy.describe(uuid1), descriptionStrategy.describe(uuid2)));
|
||||
return UpdateDescription.mentioning(Arrays.asList(uuid1, uuid2), () -> stringFactory.create(descriptionStrategy.describe(uuid1), descriptionStrategy.describe(uuid2)), lightIconResource, darkIconResource);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,24 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
@@ -20,9 +31,9 @@ public final class LiveUpdateMessage {
|
||||
* recreates the string asynchronously when they change.
|
||||
*/
|
||||
@AnyThread
|
||||
public static LiveData<String> fromMessageDescription(@NonNull UpdateDescription updateDescription) {
|
||||
public static LiveData<Spannable> fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription) {
|
||||
if (updateDescription.isStringStatic()) {
|
||||
return LiveDataUtil.just(updateDescription.getStaticString());
|
||||
return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString()));
|
||||
}
|
||||
|
||||
List<LiveData<Recipient>> allMentionedRecipients = Stream.of(updateDescription.getMentioned())
|
||||
@@ -32,16 +43,37 @@ public final class LiveUpdateMessage {
|
||||
LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object())
|
||||
: LiveDataUtil.merge(allMentionedRecipients);
|
||||
|
||||
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> updateDescription.getString());
|
||||
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes a single recipient and recreates the string asynchronously when they change.
|
||||
*/
|
||||
public static LiveData<String> recipientToStringAsync(@NonNull RecipientId recipientId,
|
||||
@NonNull Function<Recipient, String> createStringInBackground)
|
||||
public static LiveData<Spannable> recipientToStringAsync(@NonNull RecipientId recipientId,
|
||||
@NonNull Function<Recipient, Spannable> createStringInBackground)
|
||||
{
|
||||
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveData(), createStringInBackground);
|
||||
}
|
||||
|
||||
private static @NonNull Spannable toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string) {
|
||||
boolean isDarkTheme = ThemeUtil.isDarkTheme(context);
|
||||
int drawableResource = isDarkTheme ? updateDescription.getDarkIconResource() : updateDescription.getLightIconResource();
|
||||
int tint = isDarkTheme ? updateDescription.getDarkTint() : updateDescription.getLightTint();
|
||||
|
||||
if (tint == 0) {
|
||||
tint = ThemeUtil.getThemedColor(context, R.attr.conversation_item_update_text_color);
|
||||
}
|
||||
|
||||
if (drawableResource == 0) {
|
||||
return new SpannableString(string);
|
||||
} else {
|
||||
Drawable drawable = ContextCompat.getDrawable(context, drawableResource);
|
||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||
drawable.setColorFilter(tint, PorterDuff.Mode.SRC_ATOP);
|
||||
|
||||
Spannable stringWithImage = new SpannableStringBuilder().append(SpanUtil.buildImageSpan(drawable)).append(" ").append(string);
|
||||
|
||||
return new SpannableString(SpanUtil.color(tint, stringWithImage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@ import android.text.SpannableString;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -38,9 +41,11 @@ import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
@@ -48,6 +53,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -127,43 +133,56 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
if (isGroupUpdate() && isGroupV2()) {
|
||||
return getGv2ChangeDescription(context, getBody());
|
||||
} else if (isGroupUpdate() && isOutgoing()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_updated_group));
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
|
||||
} else if (isGroupUpdate()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> GroupUtil.getNonV2GroupDescription(context, getBody()).toString(r));
|
||||
return fromRecipient(getIndividualRecipient(), r -> GroupUtil.getNonV2GroupDescription(context, getBody()).toString(r), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
|
||||
} else if (isGroupQuit() && isOutgoing()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_left_group));
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_left_group), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16);
|
||||
} else if (isGroupQuit()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.ConversationItem_group_action_left, r.getDisplayName(context)));
|
||||
} else if (isIncomingCall()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you, r.getDisplayName(context)));
|
||||
} else if (isOutgoingCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_called));
|
||||
} else if (isMissedCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_call));
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.ConversationItem_group_action_left, r.getDisplayName(context)), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16);
|
||||
} else if (isIncomingAudioCall()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you_date, r.getDisplayName(context), getCallDateString(context)), R.drawable.ic_update_audio_call_incoming_light_16, R.drawable.ic_update_audio_call_incoming_dark_16);
|
||||
} else if (isIncomingVideoCall()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you_date, r.getDisplayName(context), getCallDateString(context)), R.drawable.ic_update_video_call_incomg_light_16, R.drawable.ic_update_video_call_incoming_dark_16);
|
||||
} else if (isOutgoingAudioCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_called_date, getCallDateString(context)), R.drawable.ic_update_audio_call_outgoing_light_16, R.drawable.ic_update_audio_call_outgoing_dark_16);
|
||||
} else if (isOutgoingVideoCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_called_date, getCallDateString(context)), R.drawable.ic_update_video_call_outgoing_light_16, R.drawable.ic_update_video_call_outgoing_dark_16);
|
||||
} else if (isMissedAudioCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_audio_call_date, getCallDateString(context)), R.drawable.ic_update_audio_call_missed_light_16, R.drawable.ic_update_audio_call_missed_dark_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
|
||||
} else if (isMissedVideoCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_video_call_date, getCallDateString(context)), R.drawable.ic_update_video_call_missed_light_16, R.drawable.ic_update_video_call_missed_dark_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
|
||||
} else if (isJoined()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)));
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
int seconds = (int)(getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, r.getDisplayName(context)));
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages), R.drawable.ic_update_timer_disabled_light_16, R.drawable.ic_update_timer_disabled_dark_16)
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, r.getDisplayName(context)), R.drawable.ic_update_timer_disabled_light_16, R.drawable.ic_update_timer_disabled_dark_16);
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, r.getDisplayName(context), time));
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16)
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, r.getDisplayName(context), time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16);
|
||||
} else if (isIdentityUpdate()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)));
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)), R.drawable.ic_update_safety_number_light_16, R.drawable.ic_update_safety_number_dark_16);
|
||||
} else if (isIdentityVerified()) {
|
||||
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, r.getDisplayName(context)));
|
||||
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, r.getDisplayName(context)));
|
||||
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, r.getDisplayName(context)), R.drawable.ic_update_verified_light_16, R.drawable.ic_update_verified_dark_16);
|
||||
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, r.getDisplayName(context)), R.drawable.ic_update_verified_light_16, R.drawable.ic_update_verified_dark_16);
|
||||
} else if (isIdentityDefault()) {
|
||||
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, r.getDisplayName(context)));
|
||||
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, r.getDisplayName(context)));
|
||||
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, r.getDisplayName(context)), R.drawable.ic_update_info_light_16, R.drawable.ic_update_info_dark_16);
|
||||
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, r.getDisplayName(context)), R.drawable.ic_update_info_light_16, R.drawable.ic_update_info_dark_16);
|
||||
} else if (isProfileChange()) {
|
||||
return staticUpdateDescription(getProfileChangeDescription(context));
|
||||
return staticUpdateDescription(getProfileChangeDescription(context), R.drawable.ic_update_profile_light_16, R.drawable.ic_update_profile_dark_16);
|
||||
} else if (isEndSession()) {
|
||||
if (isOutgoing()) return staticUpdateDescription(context.getString(R.string.SmsMessageRecord_secure_session_reset));
|
||||
else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)));
|
||||
if (isOutgoing()) return staticUpdateDescription(context.getString(R.string.SmsMessageRecord_secure_session_reset), R.drawable.ic_update_info_light_16, R.drawable.ic_update_info_dark_16);
|
||||
else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)), R.drawable.ic_update_info_light_16, R.drawable.ic_update_info_dark_16);
|
||||
} else if (isGroupV1MigrationEvent()) {
|
||||
if (Util.isEmpty(getBody())) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_this_group_was_updated_to_a_new_group), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16);
|
||||
} else {
|
||||
int count = getGroupV1MigrationEventInvites().size();
|
||||
return staticUpdateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_members_couldnt_be_added_to_the_new_group_and_have_been_invited, count, count), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -177,13 +196,13 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get());
|
||||
|
||||
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() != 0) {
|
||||
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getChange()));
|
||||
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange()));
|
||||
} else {
|
||||
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated));
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,12 +229,35 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient, @NonNull Function<Recipient, String> stringFunction) {
|
||||
return UpdateDescription.mentioning(Collections.singletonList(recipient.getUuid().or(UuidUtil.UNKNOWN_UUID)), () -> stringFunction.apply(recipient.resolve()));
|
||||
private @NonNull String getCallDateString(@NonNull Context context) {
|
||||
return DateUtils.getExtendedRelativeTimeSpanString(context, Locale.getDefault(), getDateSent());
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string) {
|
||||
return UpdateDescription.staticDescription(string);
|
||||
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient,
|
||||
@NonNull Function<Recipient, String> stringGenerator,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource)
|
||||
{
|
||||
return UpdateDescription.mentioning(Collections.singletonList(recipient.getUuid().or(UuidUtil.UNKNOWN_UUID)),
|
||||
() -> stringGenerator.apply(recipient.resolve()),
|
||||
lightIconResource,
|
||||
darkIconResource);
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource)
|
||||
{
|
||||
return UpdateDescription.staticDescription(string, lightIconResource, darkIconResource);
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource,
|
||||
@ColorInt int lightTint,
|
||||
@ColorInt int darkTint)
|
||||
{
|
||||
return UpdateDescription.staticDescription(string, lightIconResource, darkIconResource, lightTint, darkTint);
|
||||
}
|
||||
|
||||
private @NonNull String getProfileChangeDescription(@NonNull Context context) {
|
||||
@@ -316,9 +358,22 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
|
||||
}
|
||||
|
||||
public boolean isGroupV1MigrationEvent() {
|
||||
return SmsDatabase.Types.isGroupV1MigrationEvent(type);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> getGroupV1MigrationEventInvites() {
|
||||
if (isGroupV1MigrationEvent()) {
|
||||
return RecipientId.fromSerializedList(getBody());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUpdate() {
|
||||
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
|
||||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isProfileChange();
|
||||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
|
||||
isProfileChange() || isGroupV1MigrationEvent();
|
||||
}
|
||||
|
||||
public boolean isMediaPending() {
|
||||
|
||||
@@ -147,8 +147,12 @@ public final class ThreadRecord {
|
||||
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||
}
|
||||
|
||||
public boolean isOutgoingCall() {
|
||||
return SmsDatabase.Types.isOutgoingCall(type);
|
||||
public boolean isOutgoingAudioCall() {
|
||||
return SmsDatabase.Types.isOutgoingAudioCall(type);
|
||||
}
|
||||
|
||||
public boolean isOutgoingVideoCall() {
|
||||
return SmsDatabase.Types.isOutgoingVideoCall(type);
|
||||
}
|
||||
|
||||
public boolean isVerificationStatusChange() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
@@ -28,17 +30,29 @@ public final class UpdateDescription {
|
||||
private final Collection<UUID> mentioned;
|
||||
private final StringFactory stringFactory;
|
||||
private final String staticString;
|
||||
private final int lightIconResource;
|
||||
private final int darkIconResource;
|
||||
private final int lightTint;
|
||||
private final int darkTint;
|
||||
|
||||
private UpdateDescription(@NonNull Collection<UUID> mentioned,
|
||||
@Nullable StringFactory stringFactory,
|
||||
@Nullable String staticString)
|
||||
@Nullable String staticString,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource,
|
||||
@ColorInt int lightTint,
|
||||
@ColorInt int darkTint)
|
||||
{
|
||||
if (staticString == null && stringFactory == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
this.mentioned = mentioned;
|
||||
this.stringFactory = stringFactory;
|
||||
this.staticString = staticString;
|
||||
this.mentioned = mentioned;
|
||||
this.stringFactory = stringFactory;
|
||||
this.staticString = staticString;
|
||||
this.lightIconResource = lightIconResource;
|
||||
this.darkIconResource = darkIconResource;
|
||||
this.lightTint = lightTint;
|
||||
this.darkTint = darkTint;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,18 +63,39 @@ public final class UpdateDescription {
|
||||
* @param stringFactory The background method for generating the string.
|
||||
*/
|
||||
public static UpdateDescription mentioning(@NonNull Collection<UUID> mentioned,
|
||||
@NonNull StringFactory stringFactory)
|
||||
@NonNull StringFactory stringFactory,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource)
|
||||
{
|
||||
return new UpdateDescription(UuidUtil.filterKnown(mentioned),
|
||||
stringFactory,
|
||||
null);
|
||||
null,
|
||||
lightIconResource,
|
||||
darkIconResource,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update description that's string value is fixed.
|
||||
*/
|
||||
public static UpdateDescription staticDescription(@NonNull String staticString) {
|
||||
return new UpdateDescription(Collections.emptyList(), null, staticString);
|
||||
public static UpdateDescription staticDescription(@NonNull String staticString,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource)
|
||||
{
|
||||
return new UpdateDescription(Collections.emptyList(), null, staticString, lightIconResource, darkIconResource, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update description that's string value is fixed with a specific tint color.
|
||||
*/
|
||||
public static UpdateDescription staticDescription(@NonNull String staticString,
|
||||
@DrawableRes int lightIconResource,
|
||||
@DrawableRes int darkIconResource,
|
||||
@ColorInt int lightTint,
|
||||
@ColorInt int darkTint)
|
||||
{
|
||||
return new UpdateDescription(Collections.emptyList(), null, staticString, lightIconResource, darkIconResource, lightTint, darkTint);
|
||||
}
|
||||
|
||||
public boolean isStringStatic() {
|
||||
@@ -93,6 +128,22 @@ public final class UpdateDescription {
|
||||
return mentioned;
|
||||
}
|
||||
|
||||
public @DrawableRes int getLightIconResource() {
|
||||
return lightIconResource;
|
||||
}
|
||||
|
||||
public @DrawableRes int getDarkIconResource() {
|
||||
return darkIconResource;
|
||||
}
|
||||
|
||||
public @ColorInt int getLightTint() {
|
||||
return lightTint;
|
||||
}
|
||||
|
||||
public @ColorInt int getDarkTint() {
|
||||
return darkTint;
|
||||
}
|
||||
|
||||
public static UpdateDescription concatWithNewLines(@NonNull List<UpdateDescription> updateDescriptions) {
|
||||
if (updateDescriptions.size() == 0) {
|
||||
throw new AssertionError();
|
||||
@@ -103,7 +154,9 @@ public final class UpdateDescription {
|
||||
}
|
||||
|
||||
if (allAreStatic(updateDescriptions)) {
|
||||
return UpdateDescription.staticDescription(concatStaticLines(updateDescriptions));
|
||||
return UpdateDescription.staticDescription(concatStaticLines(updateDescriptions),
|
||||
updateDescriptions.get(0).getLightIconResource(),
|
||||
updateDescriptions.get(0).getDarkIconResource());
|
||||
}
|
||||
|
||||
Set<UUID> allMentioned = new HashSet<>();
|
||||
@@ -112,7 +165,10 @@ public final class UpdateDescription {
|
||||
allMentioned.addAll(updateDescription.getMentioned());
|
||||
}
|
||||
|
||||
return UpdateDescription.mentioning(allMentioned, () -> concatLines(updateDescriptions));
|
||||
return UpdateDescription.mentioning(allMentioned,
|
||||
() -> concatLines(updateDescriptions),
|
||||
updateDescriptions.get(0).getLightIconResource(),
|
||||
updateDescriptions.get(0).getDarkIconResource());
|
||||
}
|
||||
|
||||
private static boolean allAreStatic(@NonNull Collection<UpdateDescription> updateDescriptions) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.KbsEnclave;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV2AuthorizationMemoryValueCache;
|
||||
@@ -15,6 +16,7 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.pin.KbsEnclaves;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
@@ -111,11 +113,11 @@ public class ApplicationDependencies {
|
||||
return groupsV2Operations;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull KeyBackupService getKeyBackupService() {
|
||||
public static synchronized @NonNull KeyBackupService getKeyBackupService(@NonNull KbsEnclave enclave) {
|
||||
return getSignalServiceAccountManager().getKeyBackupService(IasKeyStore.getIasKeyStore(application),
|
||||
BuildConfig.KBS_ENCLAVE_NAME,
|
||||
Hex.fromStringOrThrow(BuildConfig.KBS_SERVICE_ID),
|
||||
BuildConfig.KBS_MRENCLAVE,
|
||||
enclave.getEnclaveName(),
|
||||
Hex.fromStringOrThrow(enclave.getServiceId()),
|
||||
enclave.getMrEnclave(),
|
||||
10);
|
||||
}
|
||||
|
||||
@@ -214,16 +216,6 @@ public class ApplicationDependencies {
|
||||
return frameRateTracker;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull KeyValueStore getKeyValueStore() {
|
||||
assertInitialization();
|
||||
|
||||
if (keyValueStore == null) {
|
||||
keyValueStore = provider.provideKeyValueStore();
|
||||
}
|
||||
|
||||
return keyValueStore;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull MegaphoneRepository getMegaphoneRepository() {
|
||||
assertInitialization();
|
||||
|
||||
@@ -281,7 +273,6 @@ public class ApplicationDependencies {
|
||||
@NonNull LiveRecipientCache provideRecipientCache();
|
||||
@NonNull JobManager provideJobManager();
|
||||
@NonNull FrameRateTracker provideFrameRateTracker();
|
||||
@NonNull KeyValueStore provideKeyValueStore();
|
||||
@NonNull MegaphoneRepository provideMegaphoneRepository();
|
||||
@NonNull EarlyMessageCache provideEarlyMessageCache();
|
||||
@NonNull MessageNotifier provideMessageNotifier();
|
||||
|
||||
@@ -99,7 +99,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
|
||||
Optional.of(new SecurityEventListener(context)),
|
||||
provideClientZkOperations().getProfileOperations(),
|
||||
SignalExecutors.newCachedBoundedExecutor("signal-messages", 1, 16));
|
||||
SignalExecutors.newCachedBoundedExecutor("signal-messages", 1, 16),
|
||||
FeatureFlags.maxEnvelopeSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,12 +154,6 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
return new FrameRateTracker(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull KeyValueStore provideKeyValueStore() {
|
||||
return new KeyValueStore(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MegaphoneRepository provideMegaphoneRepository() {
|
||||
return new MegaphoneRepository(context);
|
||||
}
|
||||
|
||||
@@ -116,4 +116,16 @@ public class CallParticipant {
|
||||
public int hashCode() {
|
||||
return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "CallParticipant{" +
|
||||
"cameraState=" + cameraState +
|
||||
", recipient=" + recipient.getId() +
|
||||
", identityKey=" + (identityKey == null ? "absent" : "present") +
|
||||
", videoSink=" + (videoSink.getEglBase() == null ? "not initialized" : "initialized") +
|
||||
", videoEnabled=" + videoEnabled +
|
||||
", microphoneEnabled=" + microphoneEnabled +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.events;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
@@ -13,6 +14,8 @@ import java.util.List;
|
||||
public class WebRtcViewModel {
|
||||
|
||||
public enum State {
|
||||
IDLE,
|
||||
|
||||
// Normal states
|
||||
CALL_PRE_JOIN,
|
||||
CALL_INCOMING,
|
||||
@@ -32,7 +35,14 @@ public class WebRtcViewModel {
|
||||
// Multiring Hangup States
|
||||
CALL_ACCEPTED_ELSEWHERE,
|
||||
CALL_DECLINED_ELSEWHERE,
|
||||
CALL_ONGOING_ELSEWHERE
|
||||
CALL_ONGOING_ELSEWHERE;
|
||||
|
||||
public boolean isErrorState() {
|
||||
return this == NETWORK_FAILURE ||
|
||||
this == RECIPIENT_UNAVAILABLE ||
|
||||
this == NO_SUCH_USER ||
|
||||
this == UNTRUSTED_IDENTITY;
|
||||
}
|
||||
}
|
||||
|
||||
private final @NonNull State state;
|
||||
@@ -48,7 +58,7 @@ public class WebRtcViewModel {
|
||||
public WebRtcViewModel(@NonNull State state,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull CameraState localCameraState,
|
||||
@NonNull BroadcastVideoSink localSink,
|
||||
@Nullable BroadcastVideoSink localSink,
|
||||
boolean isBluetoothAvailable,
|
||||
boolean isMicrophoneEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
@@ -62,7 +72,7 @@ public class WebRtcViewModel {
|
||||
this.callConnectedTime = callConnectedTime;
|
||||
this.remoteParticipants = remoteParticipants;
|
||||
|
||||
localParticipant = CallParticipant.createLocal(localCameraState, localSink, isMicrophoneEnabled);
|
||||
localParticipant = CallParticipant.createLocal(localCameraState, localSink != null ? localSink : new BroadcastVideoSink(null), isMicrophoneEnabled);
|
||||
}
|
||||
|
||||
public @NonNull State getState() {
|
||||
@@ -97,4 +107,15 @@ public class WebRtcViewModel {
|
||||
return remoteParticipants;
|
||||
}
|
||||
|
||||
@Override public @NonNull String toString() {
|
||||
return "WebRtcViewModel{" +
|
||||
"state=" + state +
|
||||
", recipient=" + recipient.getId() +
|
||||
", isBluetoothAvailable=" + isBluetoothAvailable +
|
||||
", isRemoteVideoOffer=" + isRemoteVideoOffer +
|
||||
", callConnectedTime=" + callConnectedTime +
|
||||
", localParticipant=" + localParticipant +
|
||||
", remoteParticipants=" + remoteParticipants +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
public final class GroupAlreadyExistsException extends GroupChangeException {
|
||||
|
||||
public GroupAlreadyExistsException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user