Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebbf8fad4b | ||
|
|
5891c6fb2d | ||
|
|
7c96319fb6 | ||
|
|
0d652ccfd6 | ||
|
|
d3718aa7ef | ||
|
|
fcdcb9fd33 | ||
|
|
a8f925def0 | ||
|
|
53cb125712 | ||
|
|
2a5793d96e | ||
|
|
d460fa7ed4 | ||
|
|
5272b13c41 | ||
|
|
a66ac42038 | ||
|
|
0014a2cba7 | ||
|
|
7a9c01e6e5 | ||
|
|
16402e43a5 | ||
|
|
b1944da58d | ||
|
|
9081d3c826 | ||
|
|
d4ae0ca4cb | ||
|
|
88f6ab915e | ||
|
|
939024faff | ||
|
|
4c6e7991df | ||
|
|
036d91c039 | ||
|
|
869c922532 | ||
|
|
217d15a853 | ||
|
|
931ffd0ba3 | ||
|
|
fecac297fa | ||
|
|
b0ea8d7df5 | ||
|
|
f126df2120 | ||
|
|
42450024fc | ||
|
|
101db6e164 | ||
|
|
13bef94bf7 | ||
|
|
02792c5a6f | ||
|
|
303929090b | ||
|
|
7a24554b68 | ||
|
|
5b10aa6fa7 | ||
|
|
e6eefac609 | ||
|
|
5f5a80dcbe | ||
|
|
7802448b24 | ||
|
|
16d231f718 | ||
|
|
62ca6cdd2f | ||
|
|
7d81ed1150 | ||
|
|
27812bb1ec | ||
|
|
6854f7eb2a | ||
|
|
de86c5622d | ||
|
|
6bf1a4295f | ||
|
|
7de2f0f460 | ||
|
|
50149a3803 | ||
|
|
d7ee9639fd | ||
|
|
7d5627b17b | ||
|
|
e24c951d83 | ||
|
|
e6a11c1ccf | ||
|
|
3f66981359 | ||
|
|
874f808d56 | ||
|
|
450dc2f368 | ||
|
|
7a69df42a7 | ||
|
|
1ce1e30d32 | ||
|
|
011f1d592e | ||
|
|
1d29b0166d | ||
|
|
6df1a68213 | ||
|
|
b7ee6bfcb3 | ||
|
|
c0cb2b5e12 | ||
|
|
b38865bdc7 | ||
|
|
6f46331772 | ||
|
|
989bd662c6 | ||
|
|
359e593481 | ||
|
|
b7e0fe22db | ||
|
|
61cfbd6852 | ||
|
|
02c0d3ed6e | ||
|
|
e4d6c3aeb2 | ||
|
|
0c6761fcfd | ||
|
|
8f884fdd5c | ||
|
|
07cea1818e | ||
|
|
132bc15373 | ||
|
|
d993748753 | ||
|
|
3372565a39 | ||
|
|
134ac2b2fd | ||
|
|
0e0e91b4fe | ||
|
|
25b50bdb8f | ||
|
|
1988085171 | ||
|
|
f892e9baff | ||
|
|
4828d84caf | ||
|
|
aeae6ac292 | ||
|
|
0544c1f249 | ||
|
|
5027159ed8 | ||
|
|
ce778be895 | ||
|
|
9e349d2b30 | ||
|
|
72f19758db | ||
|
|
55bce1fa12 | ||
|
|
5e1ebaa5d4 | ||
|
|
742c348998 | ||
|
|
9d46b52786 | ||
|
|
ef374952ab | ||
|
|
f8ef4d5985 | ||
|
|
85929809f0 | ||
|
|
068540120e | ||
|
|
471c4fc200 | ||
|
|
398c67362d | ||
|
|
4ceeda5f02 | ||
|
|
2bf6b993fe | ||
|
|
68363c5b82 | ||
|
|
9f47a41017 | ||
|
|
ba70101efd | ||
|
|
3aa54c9982 | ||
|
|
825ca0d737 | ||
|
|
6754fef164 | ||
|
|
4c079a8c25 | ||
|
|
6e09d101b5 | ||
|
|
39aa583297 | ||
|
|
b08db7a8c5 | ||
|
|
865bf0d056 | ||
|
|
d52c520c02 | ||
|
|
1eabf11cdb | ||
|
|
cfb16d3f17 | ||
|
|
d5707638a6 | ||
|
|
5cda5db7f7 | ||
|
|
5c5d55d265 | ||
|
|
4dd3b92eda | ||
|
|
112579079f | ||
|
|
9897ba4b28 | ||
|
|
c64dfff4c7 | ||
|
|
915b3f0cd3 | ||
|
|
c295d11fc4 | ||
|
|
bc47c5436d | ||
|
|
aaeba4efe1 | ||
|
|
3c0eb58381 | ||
|
|
c4f22449f9 | ||
|
|
bca346ec2f | ||
|
|
e0bd60f87c | ||
|
|
aeedab1531 | ||
|
|
c959f41c68 | ||
|
|
9ba755da16 | ||
|
|
34026c5538 | ||
|
|
ea64425456 | ||
|
|
eb34a20195 | ||
|
|
445513cc32 | ||
|
|
e431518a9d | ||
|
|
61df88e094 | ||
|
|
891c130e12 | ||
|
|
b4ced5278e | ||
|
|
10364e9342 | ||
|
|
74dc222a54 | ||
|
|
2e4ac7ede1 | ||
|
|
184c1b67cc | ||
|
|
f702338129 | ||
|
|
4b4b263423 | ||
|
|
83c16a46de | ||
|
|
6383896a79 | ||
|
|
5fa1560a10 | ||
|
|
9bd6ad36cc | ||
|
|
83cc7d5181 | ||
|
|
44150673e9 | ||
|
|
5092d723a8 | ||
|
|
218964cbda | ||
|
|
ccc9752485 | ||
|
|
619038f27d | ||
|
|
9f197b12ed | ||
|
|
690608cdf3 | ||
|
|
4035932340 | ||
|
|
fc9d94701c | ||
|
|
6d54ae5f3d | ||
|
|
c53abe0941 | ||
|
|
276e253fdf | ||
|
|
f160e960be | ||
|
|
41b57b9207 | ||
|
|
56eae8c7bf | ||
|
|
46c8b3b690 | ||
|
|
58b11f3c47 | ||
|
|
40b4b316b3 | ||
|
|
7a31f69aea | ||
|
|
648c99e81d | ||
|
|
56b482a26f | ||
|
|
c6df4af53a | ||
|
|
32fe927bfc | ||
|
|
5740b768d0 | ||
|
|
d8e74c730a | ||
|
|
58846bbf42 | ||
|
|
78d30fc479 | ||
|
|
86afa988a0 | ||
|
|
6104ef62df | ||
|
|
3f89acf9bd | ||
|
|
591d499462 | ||
|
|
c31a7152bc | ||
|
|
343cc3ca67 | ||
|
|
23e18cee22 | ||
|
|
5b2c458bcf | ||
|
|
c10c64a6a6 | ||
|
|
957221e118 | ||
|
|
e18e4454e4 | ||
|
|
e1067e30de | ||
|
|
09b0f15294 | ||
|
|
b1d6ff4bbd | ||
|
|
a49e9dd96d | ||
|
|
5e428e2c4d | ||
|
|
0f6ff3c101 | ||
|
|
1ade8b502f | ||
|
|
cc25f0685c | ||
|
|
e91ed88785 | ||
|
|
39bc6d5eb3 | ||
|
|
b7f472b0cd | ||
|
|
942f4a45bf | ||
|
|
767896b14c | ||
|
|
8c35628863 | ||
|
|
d555370076 | ||
|
|
bbbe76697d | ||
|
|
fc1d60e65b | ||
|
|
9dc856202a | ||
|
|
6bc41776b1 |
2
.github/workflows/android.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew qa --parallel
|
||||
run: ./gradlew qa
|
||||
|
||||
- name: Archive reports for failed build
|
||||
if: ${{ failure() }}
|
||||
|
||||
4
.github/workflows/diffuse.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Build with Gradle
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: ./gradlew assemblePlayProdRelease --parallel
|
||||
run: ./gradlew assemblePlayProdRelease
|
||||
|
||||
- name: Copy base apk
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
clean: 'false'
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assemblePlayProdRelease --parallel
|
||||
run: ./gradlew assemblePlayProdRelease
|
||||
|
||||
- name: Copy PR apk
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Signal Android
|
||||
# Signal Android
|
||||
|
||||
Signal is a simple, powerful, and secure messenger.
|
||||
|
||||
@@ -54,7 +54,7 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2023 Signal
|
||||
Copyright 2013-2024 Signal Messenger, LLC
|
||||
|
||||
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
|
||||
2
apntool/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*.db
|
||||
*.db.gz
|
||||
@@ -1,106 +0,0 @@
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
import sqlite3
|
||||
import gzip
|
||||
from progressbar import ProgressBar, Counter, Timer
|
||||
from lxml import etree
|
||||
|
||||
parser = argparse.ArgumentParser(prog='apntool', description="""Process Android's apn xml files and drop them into an
|
||||
easily queryable SQLite db. Tested up to version 9 of
|
||||
their APN file.""")
|
||||
parser.add_argument('-v', '--version', action='version', version='%(prog)s v1.1')
|
||||
parser.add_argument('-i', '--input', help='the xml file to parse', default='apns.xml', required=False)
|
||||
parser.add_argument('-o', '--output', help='the sqlite db output file', default='apns.db', required=False)
|
||||
parser.add_argument('--quiet', help='do not show progress or verbose instructions', action='store_true', required=False)
|
||||
parser.add_argument('--no-gzip', help="do not gzip after creation", action='store_true', required=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def normalized(target):
|
||||
o2_typo = re.compile(r"02\.co\.uk")
|
||||
port_typo = re.compile(r"(\d+\.\d+\.\d+\.\d+)\.(\d+)")
|
||||
leading_zeros = re.compile(r"(/|\.|^)0+(\d+)")
|
||||
subbed = o2_typo.sub(r'o2.co.uk', target)
|
||||
subbed = port_typo.sub(r'\1:\2', subbed)
|
||||
subbed = leading_zeros.sub(r'\1\2', subbed)
|
||||
return subbed
|
||||
|
||||
try:
|
||||
connection = sqlite3.connect(args.output)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT SQLITE_VERSION()')
|
||||
version = cursor.fetchone()
|
||||
if not args.quiet:
|
||||
print("SQLite version: %s" % version)
|
||||
print("Opening %s" % args.input)
|
||||
|
||||
cursor.execute("PRAGMA legacy_file_format=ON")
|
||||
cursor.execute("PRAGMA journal_mode=DELETE")
|
||||
cursor.execute("PRAGMA page_size=32768")
|
||||
cursor.execute("VACUUM")
|
||||
cursor.execute("DROP TABLE IF EXISTS apns")
|
||||
cursor.execute("""CREATE TABLE apns(_id INTEGER PRIMARY KEY, mccmnc TEXT, mcc TEXT, mnc TEXT, carrier TEXT,
|
||||
apn TEXT, mmsc TEXT, port INTEGER, type TEXT, protocol TEXT, bearer TEXT, roaming_protocol TEXT,
|
||||
carrier_enabled INTEGER, mmsproxy TEXT, mmsport INTEGER, proxy TEXT, mvno_match_data TEXT,
|
||||
mvno_type TEXT, authtype INTEGER, user TEXT, password TEXT, server TEXT)""")
|
||||
|
||||
apns = etree.parse(args.input)
|
||||
root = apns.getroot()
|
||||
pbar = None
|
||||
if not args.quiet:
|
||||
pbar = ProgressBar(widgets=['Processed: ', Counter(), ' apns (', Timer(), ')'], maxval=len(list(root))).start()
|
||||
|
||||
count = 0
|
||||
for apn in root.iter("apn"):
|
||||
if apn.get("mmsc") is None:
|
||||
continue
|
||||
sqlvars = ["?" for x in apn.attrib.keys()] + ["?"]
|
||||
mccmnc = "%s%s" % (apn.get("mcc"), apn.get("mnc"))
|
||||
normalized_mmsc = normalized(apn.get("mmsc"))
|
||||
if normalized_mmsc != apn.get("mmsc"):
|
||||
print("normalize MMSC: %s => %s" % (apn.get("mmsc"), normalized_mmsc))
|
||||
apn.set("mmsc", normalized_mmsc)
|
||||
|
||||
if not apn.get("mmsproxy") is None:
|
||||
normalized_mmsproxy = normalized(apn.get("mmsproxy"))
|
||||
if normalized_mmsproxy != apn.get("mmsproxy"):
|
||||
print("normalize proxy: %s => %s" % (apn.get("mmsproxy"), normalized_mmsproxy))
|
||||
apn.set("mmsproxy", normalized_mmsproxy)
|
||||
|
||||
values = [apn.get(attrib) for attrib in apn.attrib.keys()] + [mccmnc]
|
||||
keys = apn.attrib.keys() + ["mccmnc"]
|
||||
|
||||
cursor.execute("SELECT 1 FROM apns WHERE mccmnc = ? AND apn = ?", [mccmnc, apn.get("apn")])
|
||||
if cursor.fetchone() is None:
|
||||
statement = "INSERT INTO apns (%s) VALUES (%s)" % (", ".join(keys), ", ".join(sqlvars))
|
||||
cursor.execute(statement, values)
|
||||
|
||||
count += 1
|
||||
if not args.quiet:
|
||||
pbar.update(count)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.finish()
|
||||
connection.commit()
|
||||
print("Successfully written to %s" % args.output)
|
||||
|
||||
if not args.no_gzip:
|
||||
gzipped_file = "%s.gz" % (args.output,)
|
||||
with open(args.output, 'rb') as orig:
|
||||
with gzip.open(gzipped_file, 'wb') as gzipped:
|
||||
gzipped.writelines(orig)
|
||||
print("Successfully gzipped to %s" % gzipped_file)
|
||||
|
||||
if not args.quiet:
|
||||
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.")
|
||||
print("If you support API 10 or lower, you must use the gzipped version to avoid corruption.")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
if connection:
|
||||
connection.rollback()
|
||||
print("Error: %s" % e.args[0])
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if connection:
|
||||
connection.close()
|
||||
@@ -1,3 +0,0 @@
|
||||
argparse>=1.2.1
|
||||
lxml>=3.3.3
|
||||
progressbar-latest>=2.4
|
||||
@@ -21,8 +21,8 @@ plugins {
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
|
||||
val canonicalVersionCode = 1396
|
||||
val canonicalVersionName = "7.0.2"
|
||||
val canonicalVersionCode = 1403
|
||||
val canonicalVersionName = "7.2.2"
|
||||
|
||||
val postFixSize = 100
|
||||
val abiPostFix: Map<String, Int> = mapOf(
|
||||
@@ -40,8 +40,6 @@ val selectableVariants = listOf(
|
||||
"nightlyProdPerf",
|
||||
"nightlyProdRelease",
|
||||
"nightlyStagingRelease",
|
||||
"nightlyPnpPerf",
|
||||
"nightlyPnpRelease",
|
||||
"playProdDebug",
|
||||
"playProdSpinner",
|
||||
"playProdCanary",
|
||||
@@ -54,8 +52,6 @@ val selectableVariants = listOf(
|
||||
"playStagingSpinner",
|
||||
"playStagingPerf",
|
||||
"playStagingInstrumentation",
|
||||
"playPnpDebug",
|
||||
"playPnpSpinner",
|
||||
"playStagingRelease",
|
||||
"websiteProdSpinner",
|
||||
"websiteProdRelease"
|
||||
@@ -201,10 +197,9 @@ android {
|
||||
buildConfigField("String[]", "SIGNAL_SVR2_IPS", rootProject.extra["svr2_ips"] as String)
|
||||
buildConfigField("String", "SIGNAL_AGENT", "\"OWA\"")
|
||||
buildConfigField("String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE_DEPRECATED", "\"6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I=\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
|
||||
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O\"")
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().map { "\"$it\"" }.joinToString(separator = ", ")} }")
|
||||
@@ -213,6 +208,7 @@ android {
|
||||
buildConfigField("String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"")
|
||||
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\"")
|
||||
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"")
|
||||
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.PRODUCTION")
|
||||
|
||||
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"")
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
|
||||
@@ -220,6 +216,7 @@ android {
|
||||
buildConfigField("String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"")
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
@@ -382,27 +379,19 @@ android {
|
||||
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE_DEPRECATED", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM=\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")
|
||||
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8\"")
|
||||
buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"")
|
||||
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"")
|
||||
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"")
|
||||
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.STAGING")
|
||||
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
|
||||
}
|
||||
|
||||
create("pnp") {
|
||||
dimension = "environment"
|
||||
|
||||
initWith(getByName("staging"))
|
||||
applicationIdSuffix = ".pnp"
|
||||
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Pnp\"")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,6 +501,7 @@ dependencies {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.camera.core)
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.extensions)
|
||||
implementation(libs.androidx.camera.lifecycle)
|
||||
implementation(libs.androidx.camera.view)
|
||||
implementation(libs.androidx.concurrent.futures)
|
||||
@@ -550,10 +540,6 @@ dependencies {
|
||||
implementation(libs.android.tooltips) {
|
||||
exclude(group = "com.android.support", module = "appcompat-v7")
|
||||
}
|
||||
implementation(libs.android.smsmms) {
|
||||
exclude(group = "com.squareup.okhttp", module = "okhttp")
|
||||
exclude(group = "com.squareup.okhttp", module = "okhttp-urlconnection")
|
||||
}
|
||||
implementation(libs.stream)
|
||||
implementation(libs.lottie)
|
||||
implementation(libs.signal.android.database.sqlcipher)
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.whispersystems.** { *; }
|
||||
-keep class org.signal.libsignal.net.** { *; }
|
||||
-keep class org.signal.libsignal.protocol.** { *; }
|
||||
-keep class org.signal.libsignal.usernames.** { *; }
|
||||
-keep class org.thoughtcrime.securesms.** { *; }
|
||||
-keep class org.signal.donations.json.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
|
||||
@@ -35,4 +35,18 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
|
||||
LogDatabase.getInstance(this).logs.trimToSize()
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginJobLoop() = Unit
|
||||
|
||||
/**
|
||||
* Some of the jobs can interfere with some of the instrumentation tests.
|
||||
*
|
||||
* For example, we may try to create a release channel recipient while doing
|
||||
* an import/backup test.
|
||||
*
|
||||
* This can be used to start the job loop if needed for tests that rely on it.
|
||||
*/
|
||||
fun beginJobLoopForTests() {
|
||||
super.beginJobLoop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.whispersystems.signalservice.api.util.toByteArray
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
object TestRecipientUtils {
|
||||
|
||||
private var upperGenAci = 13131313L
|
||||
private var lowerGenAci = 0L
|
||||
|
||||
private var upperGenPni = 12121212L
|
||||
private var lowerGenPni = 0L
|
||||
|
||||
private var groupMasterKeyRandom = Random(12345)
|
||||
|
||||
fun generateProfileKey(): ByteArray {
|
||||
return ProfileKeyUtil.createNew().serialize()
|
||||
}
|
||||
|
||||
fun nextPni(): ByteArray {
|
||||
synchronized(this) {
|
||||
lowerGenPni++
|
||||
var uuid = UUID(upperGenPni, lowerGenPni)
|
||||
return uuid.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun nextAci(): ByteArray {
|
||||
synchronized(this) {
|
||||
lowerGenAci++
|
||||
var uuid = UUID(upperGenAci, lowerGenAci)
|
||||
return uuid.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun generateGroupMasterKey(): ByteArray {
|
||||
val masterKey = ByteArray(32)
|
||||
groupMasterKeyRandom.nextBytes(masterKey)
|
||||
return masterKey
|
||||
}
|
||||
}
|
||||
@@ -51,18 +51,16 @@ class AttachmentTableTest {
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||
false
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment2,
|
||||
createMediaStream(byteArrayOf(1, 2, 3)),
|
||||
false
|
||||
createMediaStream(byteArrayOf(1, 2, 3))
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
|
||||
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
|
||||
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
@@ -79,18 +77,16 @@ class AttachmentTableTest {
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||
true
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment2,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4)),
|
||||
true
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4))
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
|
||||
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
|
||||
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
@@ -121,15 +117,14 @@ class AttachmentTableTest {
|
||||
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false)
|
||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData))
|
||||
|
||||
// THEN
|
||||
val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val previousInfo = SignalDatabase.attachments.getDataFileInfo(previousDatabaseAttachmentId)!!
|
||||
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||
|
||||
assertNotEquals(standardInfo, highInfo)
|
||||
standardInfo.file assertIs previousInfo.file
|
||||
highInfo.file assertIsNot standardInfo.file
|
||||
highInfo.file.exists() assertIs true
|
||||
}
|
||||
@@ -158,9 +153,9 @@ class AttachmentTableTest {
|
||||
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
|
||||
|
||||
// THEN
|
||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val secondHighInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(secondHighDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||
val secondHighInfo = SignalDatabase.attachments.getDataFileInfo(secondHighDatabaseAttachment.attachmentId)!!
|
||||
|
||||
highInfo.file assertIsNot standardInfo.file
|
||||
secondHighInfo.file assertIs highInfo.file
|
||||
|
||||
@@ -0,0 +1,804 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MediaStream
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Collection of [AttachmentTable] tests focused around deduping logic.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AttachmentTableTest_deduping {
|
||||
|
||||
companion object {
|
||||
val DATA_A = byteArrayOf(1, 2, 3)
|
||||
val DATA_A_COMPRESSED = byteArrayOf(4, 5, 6)
|
||||
val DATA_A_HASH = byteArrayOf(1, 1, 1)
|
||||
|
||||
val DATA_B = byteArrayOf(7, 8, 9)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalStore.account().setAci(ServiceId.ACI.from(UUID.randomUUID()))
|
||||
SignalStore.account().setPni(ServiceId.PNI.from(UUID.randomUUID()))
|
||||
SignalStore.account().setE164("+15558675309")
|
||||
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates two different files with different data. Should not dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun differentFiles() {
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_B)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts files with identical data but with transform properties that make them incompatible. Should not dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun identicalFiles_incompatibleTransforms() {
|
||||
// Non-matching qualities
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim flag
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties())
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim start time
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim end time
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 1))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching mp4 fast start
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(mp4FastStart = true))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(mp4FastStart = false))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts files with identical data and compatible transform properties. Should dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun identicalFiles_compatibleTransforms() {
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through various scenarios where files are compressed and uploaded.
|
||||
*/
|
||||
@Test
|
||||
fun compressionAndUploads() {
|
||||
// Matches after the first is compressed, skip transform properly set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Matches after the first is uploaded, skip transform and ending hash properly set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Mimics sending two files at once. Ensures all fields are kept in sync as we compress and upload.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
|
||||
upload(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Re-use the upload when uploaded recently
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Do not re-use old uploads
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis() - 100.days.inWholeMilliseconds)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
|
||||
assertDoesNotHaveRemoteFields(id2)
|
||||
}
|
||||
|
||||
// This isn't so much "desirable behavior" as it is documenting how things work.
|
||||
// If an attachment is compressed but not uploaded yet, it will have a DATA_HASH_START that doesn't match the actual file content.
|
||||
// This means that if we insert a new attachment with data that matches the compressed data, we won't find a match.
|
||||
// This is ok because we don't allow forwarding unsent messages, so the chances of the user somehow sending a file that matches data we compressed are very low.
|
||||
// What *is* more common is that the user may send DATA_A again, and in this case we will still catch the dedupe (which is already tested above).
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you forward an already-send compressed attachment. We should match, skip transform, and skip upload.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you edited a video, sent it, then forwarded it. We should match, skip transform, and skip upload.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you edited a video, sent it, then forwarded it, but *edited the forwarded video*. We should not dedupe.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, false)
|
||||
assertDoesNotHaveRemoteFields(id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you sent an image using standard quality, then forwarded it using high quality.
|
||||
// Since you're forwarding, it doesn't matter if the new thing has a higher quality, we should still match and skip transform.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you sent an image using high quality, then forwarded it using standard quality.
|
||||
// Since you're forwarding, it doesn't matter if the new thing has a lower quality, we should still match and skip transform.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Make sure that files marked as unhashable are all updated together
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
upload(id2)
|
||||
clearHashes(id1)
|
||||
clearHashes(id2)
|
||||
|
||||
val file = dataFile(id1)
|
||||
SignalDatabase.attachments.markDataFileAsUnhashable(file)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id1)!!
|
||||
assertTrue(dataFileInfo.hashEnd!!.startsWith("UNHASHABLE-"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Various deletion scenarios to ensure that duped files don't deleted while there's still references.
|
||||
*/
|
||||
@Test
|
||||
fun deletions() {
|
||||
// Delete original then dupe
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
|
||||
delete(id1)
|
||||
|
||||
assertDeleted(id1)
|
||||
assertRowAndFileExists(id2)
|
||||
assertTrue(dataFile.exists())
|
||||
|
||||
delete(id2)
|
||||
|
||||
assertDeleted(id2)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
|
||||
// Delete dupe then original
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
|
||||
delete(id2)
|
||||
assertDeleted(id2)
|
||||
assertRowAndFileExists(id1)
|
||||
assertTrue(dataFile.exists())
|
||||
|
||||
delete(id1)
|
||||
assertDeleted(id1)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
|
||||
// Delete original after it was compressed
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
delete(id1)
|
||||
|
||||
assertDeleted(id1)
|
||||
assertRowAndFileExists(id2)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Quotes are weak references and should not prevent us from deleting the file
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
delete(id1)
|
||||
assertDeleted(id1)
|
||||
assertRowExists(id2)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun quotes() {
|
||||
// Basic quote deduping
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Making sure remote fields carry
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
upload(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Making sure things work for quotes of videos, which have trickier transform properties
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, transformProperties = TransformProperties.forVideoTrim(1, 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1)
|
||||
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suite of tests around the migration where we hash all of the attachments and potentially dedupe them.
|
||||
*/
|
||||
@Test
|
||||
fun migration() {
|
||||
// Verifying that getUnhashedDataFile only returns if there's actually missing hashes
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// Verifying that getUnhashedDataFile finds the missing hash
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
clearHashes(id)
|
||||
assertNotNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// Verifying that getUnhashedDataFile doesn't return if the file isn't done downloading
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
setTransferState(id, AttachmentTable.TRANSFER_PROGRESS_PENDING)
|
||||
clearHashes(id)
|
||||
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// If two attachments share the same file, when we backfill the hash, make sure both get their hashes set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
upload(id2)
|
||||
|
||||
clearHashes(id1)
|
||||
clearHashes(id2)
|
||||
|
||||
val file = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file, DATA_A_HASH)
|
||||
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Creates a situation where two different attachments have the same data but wrote to different files, and verifies the migration dedupes it
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id2)
|
||||
clearHashes(id2)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertFalse(file2.exists())
|
||||
}
|
||||
|
||||
// We've got three files now with the same data, with two of them sharing a file. We want to make sure *both* entries that share the same file get deduped.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val id3 = insertWithData(DATA_A)
|
||||
upload(id2)
|
||||
upload(id3)
|
||||
clearHashes(id2)
|
||||
clearHashes(id3)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataFilesAreTheSame(id2, id3)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertDataHashEndMatches(id2, id3)
|
||||
assertFalse(file2.exists())
|
||||
}
|
||||
|
||||
// We don't want to mess with files that are still downloading, so this makes sure that even if data matches, we don't dedupe and don't delete the file
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
// *not* uploaded
|
||||
clearHashes(id2)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertTrue(file2.exists())
|
||||
}
|
||||
}
|
||||
|
||||
private class TestContext {
|
||||
fun insertWithData(data: ByteArray, transformProperties: TransformProperties = TransformProperties.empty()): AttachmentId {
|
||||
val uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
||||
|
||||
val attachment = UriAttachmentBuilder.build(
|
||||
id = Random.nextLong(),
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
transformProperties = transformProperties
|
||||
)
|
||||
|
||||
return SignalDatabase.attachments.insertAttachmentForPreUpload(attachment).attachmentId
|
||||
}
|
||||
|
||||
fun insertQuote(attachmentId: AttachmentId): AttachmentId {
|
||||
val originalAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.self())
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(
|
||||
threadRecipient = Recipient.self(),
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
body = "some text",
|
||||
outgoingQuote = QuoteModel(
|
||||
id = 123,
|
||||
author = Recipient.self().id,
|
||||
text = "Some quote text",
|
||||
isOriginalMissing = false,
|
||||
attachments = listOf(originalAttachment),
|
||||
mentions = emptyList(),
|
||||
type = QuoteModel.Type.NORMAL,
|
||||
bodyRanges = null
|
||||
)
|
||||
),
|
||||
threadId = threadId,
|
||||
forceSms = false,
|
||||
insertListener = null
|
||||
)
|
||||
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
|
||||
return attachments[0].attachmentId
|
||||
}
|
||||
|
||||
fun compress(attachmentId: AttachmentId, newData: ByteArray, mp4FastStart: Boolean = false) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
SignalDatabase.attachments.updateAttachmentData(databaseAttachment, newData.asMediaStream())
|
||||
SignalDatabase.attachments.markAttachmentAsTransformed(attachmentId, withFastStart = mp4FastStart)
|
||||
}
|
||||
|
||||
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createPointerAttachment(attachmentId, uploadTimestamp), uploadTimestamp)
|
||||
}
|
||||
|
||||
fun delete(attachmentId: AttachmentId) {
|
||||
SignalDatabase.attachments.deleteAttachment(attachmentId)
|
||||
}
|
||||
|
||||
fun dataFile(attachmentId: AttachmentId): File {
|
||||
return SignalDatabase.attachments.getDataFileInfo(attachmentId)!!.file
|
||||
}
|
||||
|
||||
fun setTransferState(attachmentId: AttachmentId, transferState: Int) {
|
||||
// messageId doesn't actually matter -- that's for notifying listeners
|
||||
SignalDatabase.attachments.setTransferState(messageId = -1, attachmentId = attachmentId, transferState = transferState)
|
||||
}
|
||||
|
||||
fun clearHashes(id: AttachmentId) {
|
||||
SignalDatabase.attachments.writableDatabase
|
||||
.update(AttachmentTable.TABLE_NAME)
|
||||
.values(
|
||||
AttachmentTable.DATA_HASH_START to null,
|
||||
AttachmentTable.DATA_HASH_END to null
|
||||
)
|
||||
.where("${AttachmentTable.ID} = ?", id)
|
||||
.run()
|
||||
}
|
||||
|
||||
fun assertDeleted(attachmentId: AttachmentId) {
|
||||
assertNull("$attachmentId exists, but it shouldn't!", SignalDatabase.attachments.getAttachment(attachmentId))
|
||||
}
|
||||
|
||||
fun assertRowAndFileExists(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
assertNotNull("$attachmentId does not exist!", databaseAttachment)
|
||||
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(attachmentId)
|
||||
assertTrue("The file for $attachmentId does not exist!", dataFileInfo!!.file.exists())
|
||||
}
|
||||
|
||||
fun assertRowExists(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
assertNotNull("$attachmentId does not exist!", databaseAttachment)
|
||||
}
|
||||
|
||||
fun assertDataFilesAreTheSame(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assert(lhsInfo.file.exists())
|
||||
assert(rhsInfo.file.exists())
|
||||
|
||||
assertEquals(lhsInfo.file, rhsInfo.file)
|
||||
assertEquals(lhsInfo.length, rhsInfo.length)
|
||||
assertArrayEquals(lhsInfo.random, rhsInfo.random)
|
||||
}
|
||||
|
||||
fun assertDataFilesAreDifferent(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assert(lhsInfo.file.exists())
|
||||
assert(rhsInfo.file.exists())
|
||||
|
||||
assertNotEquals(lhsInfo.file, rhsInfo.file)
|
||||
}
|
||||
|
||||
fun assertDataHashStartMatches(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assertNotNull(lhsInfo.hashStart)
|
||||
assertEquals("DATA_HASH_START's did not match!", lhsInfo.hashStart, rhsInfo.hashStart)
|
||||
}
|
||||
|
||||
fun assertDataHashEndMatches(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assertNotNull(lhsInfo.hashEnd)
|
||||
assertEquals("DATA_HASH_END's did not match!", lhsInfo.hashEnd, rhsInfo.hashEnd)
|
||||
}
|
||||
|
||||
fun assertDataHashEnd(id: AttachmentId, byteArray: ByteArray) {
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id)!!
|
||||
assertArrayEquals(byteArray, Base64.decode(dataFileInfo.hashEnd!!))
|
||||
}
|
||||
|
||||
fun assertRemoteFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
|
||||
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
|
||||
|
||||
assertEquals(lhsAttachment.remoteLocation, rhsAttachment.remoteLocation)
|
||||
assertEquals(lhsAttachment.remoteKey, rhsAttachment.remoteKey)
|
||||
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
|
||||
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
|
||||
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
|
||||
assertEquals(lhsAttachment.cdnNumber, rhsAttachment.cdnNumber)
|
||||
}
|
||||
|
||||
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
assertEquals(0, databaseAttachment.uploadTimestamp)
|
||||
assertNull(databaseAttachment.remoteLocation)
|
||||
assertNull(databaseAttachment.remoteDigest)
|
||||
assertNull(databaseAttachment.remoteKey)
|
||||
assertEquals(0, databaseAttachment.cdnNumber)
|
||||
}
|
||||
|
||||
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
|
||||
val transformProperties = SignalDatabase.attachments.getTransformProperties(attachmentId)!!
|
||||
assertEquals("Incorrect skipTransform!", transformProperties.skipTransform, state)
|
||||
}
|
||||
|
||||
private fun ByteArray.asMediaStream(): MediaStream {
|
||||
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
|
||||
}
|
||||
|
||||
private fun createPointerAttachment(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): PointerAttachment {
|
||||
val location = "somewhere-${Random.nextLong()}"
|
||||
val key = "somekey-${Random.nextLong()}"
|
||||
val digest = Random.nextBytes(32)
|
||||
val incrementalDigest = Random.nextBytes(16)
|
||||
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
|
||||
return PointerAttachment(
|
||||
"image/jpeg",
|
||||
AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
databaseAttachment.size, // size
|
||||
null,
|
||||
3, // cdnNumber
|
||||
location,
|
||||
key,
|
||||
digest,
|
||||
incrementalDigest,
|
||||
5, // incrementalMacChunkSize
|
||||
null,
|
||||
databaseAttachment.voiceNote,
|
||||
databaseAttachment.borderless,
|
||||
databaseAttachment.videoGif,
|
||||
databaseAttachment.width,
|
||||
databaseAttachment.height,
|
||||
uploadTimestamp,
|
||||
databaseAttachment.caption,
|
||||
databaseAttachment.stickerLocator,
|
||||
databaseAttachment.blurHash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(content: TestContext.() -> Unit) {
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
val context = TestContext()
|
||||
context.content()
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class RecipientTableTest {
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!!
|
||||
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Hidden", false))!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class RecipientTableTest {
|
||||
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
||||
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
||||
|
||||
val results = SignalDatabase.recipients.querySignalContacts("Blocked", false)!!
|
||||
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", false))!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
|
||||
@@ -776,6 +776,18 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectThreadMergeEvent(E164_A)
|
||||
}
|
||||
|
||||
test("merge, e164+pni & e164+aci, pni+aci provided, change number") {
|
||||
given(E164_A, PNI_A, null)
|
||||
given(E164_B, null, ACI_A)
|
||||
|
||||
process(null, PNI_A, ACI_A)
|
||||
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectThreadMergeEvent(E164_A)
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
|
||||
test("merge, e164 + pni reassigned, aci abandoned") {
|
||||
given(E164_A, PNI_A, ACI_A)
|
||||
given(E164_B, PNI_B, ACI_B)
|
||||
|
||||
@@ -37,7 +37,7 @@ import java.util.Optional
|
||||
*
|
||||
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess].
|
||||
*/
|
||||
class InstrumentationApplicationDependencyProvider(application: Application, default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
|
||||
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
|
||||
|
||||
private val serviceTrustStore: TrustStore
|
||||
private val uncensoredConfiguration: SignalServiceConfiguration
|
||||
|
||||
@@ -52,7 +52,7 @@ class MessageContentProcessor__recipientStatusTest {
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ class MessageContentProcessor__recipientStatusTest {
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
private lateinit var group: GroupTestingUtils.TestGroupInfo
|
||||
private lateinit var processor: MessageContentProcessor
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
alice = harness.others[0]
|
||||
bob = harness.others[1]
|
||||
group = harness.group!!
|
||||
|
||||
processor = MessageContentProcessor(harness.context)
|
||||
|
||||
val threadIdSlot = slot<Long>()
|
||||
mockkStatic(ThreadUpdateJob::class)
|
||||
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
|
||||
SignalDatabase.threads.update(threadIdSlot.captured, false)
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic(ThreadUpdateJob::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessage() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp, alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessageMissingTimestamp() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEdits() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp)
|
||||
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
|
||||
|
||||
val message2Timestamp = messageHelper.incomingMedia().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEditsInGroup() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText(sender = alice, destination = group.recipientId).timestamp
|
||||
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp)
|
||||
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = alice, destination = group.recipientId).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = alice, destination = group.recipientId).timestamp
|
||||
|
||||
val message2Timestamp = messageHelper.incomingMedia(sender = bob, destination = group.recipientId).timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(group.recipientId)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(bob to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
private inner class MessageHelper(var startTime: Long = System.currentTimeMillis()) {
|
||||
|
||||
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzStickerMediaMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.editTextMessage(
|
||||
targetTimestamp = targetTimestamp,
|
||||
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
).dataMessage!!
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
|
||||
startTime += 1000
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
}
|
||||
|
||||
private data class MessageData(val serverGuid: UUID = UUID.randomUUID(), val timestamp: Long)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
@@ -9,6 +10,7 @@ import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
@@ -46,5 +48,8 @@ object GroupTestingUtils {
|
||||
return member(aci = requireAci())
|
||||
}
|
||||
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) {
|
||||
val groupV2Context: GroupContextV2
|
||||
get() = GroupContextV2(masterKey = masterKey.serialize().toByteString(), revision = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
@@ -33,22 +34,22 @@ object MessageContentFuzzer {
|
||||
/**
|
||||
* Create an [Envelope].
|
||||
*/
|
||||
fun envelope(timestamp: Long): Envelope {
|
||||
fun envelope(timestamp: Long, serverGuid: UUID = UUID.randomUUID()): Envelope {
|
||||
return Envelope.Builder()
|
||||
.timestamp(timestamp)
|
||||
.serverTimestamp(timestamp + 5)
|
||||
.serverGuid(UUID.randomUUID().toString())
|
||||
.serverGuid(serverGuid.toString())
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create metadata to match an [Envelope].
|
||||
*/
|
||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId, sourceDeviceId: Int = 1, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
||||
return EnvelopeMetadata(
|
||||
sourceServiceId = Recipient.resolved(source).requireServiceId(),
|
||||
sourceE164 = null,
|
||||
sourceDeviceId = 1,
|
||||
sourceDeviceId = sourceDeviceId,
|
||||
sealedSender = true,
|
||||
groupId = groupId?.decodedId,
|
||||
destinationServiceId = Recipient.resolved(destination).requireServiceId()
|
||||
@@ -60,10 +61,11 @@ object MessageContentFuzzer {
|
||||
* - An expire timer value
|
||||
* - Bold style body ranges
|
||||
*/
|
||||
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
|
||||
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
timestamp = sentTimestamp
|
||||
body = string()
|
||||
if (random.nextBoolean()) {
|
||||
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
||||
@@ -87,6 +89,20 @@ object MessageContentFuzzer {
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an edit message.
|
||||
*/
|
||||
fun editTextMessage(targetTimestamp: Long, editedDataMessage: DataMessage): Content {
|
||||
return Content.Builder()
|
||||
.editMessage(
|
||||
EditMessage.Builder().buildWith {
|
||||
targetSentTimestamp = targetTimestamp
|
||||
dataMessage = editedDataMessage
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync sent text message for the given [DataMessage].
|
||||
*/
|
||||
@@ -116,6 +132,24 @@ object MessageContentFuzzer {
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync reads message for the given [RecipientId] and message timestamp pairings.
|
||||
*/
|
||||
fun syncReadsMessage(timestamps: List<Pair<RecipientId, Long>>): Content {
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage.Builder().buildWith {
|
||||
read = timestamps.map { (senderId, timestamp) ->
|
||||
SyncMessage.Read.Builder().buildWith {
|
||||
this.senderAci = Recipient.resolved(senderId).requireAci().toString()
|
||||
this.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that may be:
|
||||
* - A text body
|
||||
@@ -184,22 +218,21 @@ object MessageContentFuzzer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that can never contain a text body. It may be:
|
||||
* - A sticker
|
||||
* Create a random media message that contains a sticker.
|
||||
*/
|
||||
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
fun fuzzStickerMediaMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
if (random.nextFloat() < 0.9) {
|
||||
sticker = DataMessage.Sticker.Builder().buildWith {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data_ = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
}
|
||||
timestamp = sentTimestamp
|
||||
sticker = DataMessage.Sticker.Builder().buildWith {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data_ = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
}
|
||||
groupV2 = groupContextV2
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
|
||||
@@ -100,7 +100,7 @@ object TestUsers {
|
||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:usesCleartextTraffic"
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
|
||||
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
||||
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||
@@ -45,16 +43,10 @@
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
|
||||
|
||||
<!-- For sending/receiving events -->
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
||||
|
||||
|
||||
<!-- Normal -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
@@ -71,17 +63,14 @@
|
||||
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<!-- For fixing MMS -->
|
||||
<!-- For device transfer -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
|
||||
<!-- Set image as wallpaper -->
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
|
||||
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
@@ -174,10 +163,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".preferences.MmsPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false" />
|
||||
|
||||
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
@@ -719,6 +704,7 @@
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:exported="false"/>
|
||||
|
||||
@@ -940,12 +926,12 @@
|
||||
</activity>
|
||||
|
||||
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:permission="android.permission.CALL_PHONE"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:permission="android.permission.CALL_PHONE"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -976,11 +962,20 @@
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".backup.v2.ui.MessageBackupsTestRestoreActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".profiles.manage.EditProfileActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".nicknames.NicknameActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".payments.preferences.PaymentsActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
@@ -1084,6 +1079,16 @@
|
||||
android:theme="@style/Theme.Signal.WallpaperCropper"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrImageSelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrScannerActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".reactions.edit.EditReactionsActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@@ -1140,19 +1145,6 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name=".service.QuickResponseService"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".service.AccountAuthenticatorService" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
@@ -1190,13 +1182,6 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".service.SmsDeliveryListener"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.services.MESSAGE_SENT"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.MarkReadReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
@@ -1256,11 +1241,6 @@
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<provider android:name=".providers.MmsBodyProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
android:authorities="${applicationId}.mms" />
|
||||
|
||||
<provider android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 100 KiB |
BIN
app/src/main/assets/fonts/SignalSymbols-Bold.otf
Normal file
@@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
|
||||
@@ -190,7 +191,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(this::initializeCleanup)
|
||||
.addNonBlocking(this::initializeGlideCodecs)
|
||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||
.addNonBlocking(this::beginJobLoop)
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
@@ -198,7 +199,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(this::initializeTrimThreadsByDateManager)
|
||||
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
@@ -216,6 +216,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
|
||||
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupRingCleanupJob::enqueue)
|
||||
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -274,7 +275,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
public void checkBuildExpiration() {
|
||||
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build expired!");
|
||||
SignalStore.misc().markClientDeprecated();
|
||||
SignalStore.misc().setClientDeprecated(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,6 +464,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void beginJobLoop() {
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void initializeBlobProvider() {
|
||||
BlobProvider.getInstance().initialize(this);
|
||||
|
||||
@@ -73,8 +73,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
int displayMode = includeSms ? ContactSelectionDisplayMode.FLAG_ALL : ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
|
||||
int displayMode = ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
}
|
||||
|
||||
|
||||
@@ -890,11 +890,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
return ContactSearchConfiguration.build(builder -> {
|
||||
builder.setQuery(contactSearchState.getQuery());
|
||||
|
||||
if (newConversationCallback != null) {
|
||||
if (newConversationCallback != null && !hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
|
||||
}
|
||||
|
||||
if (findByCallback != null) {
|
||||
if (findByCallback != null && !hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_USERNAME.getCode());
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_PHONE_NUMBER.getCode());
|
||||
}
|
||||
@@ -913,12 +913,14 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
));
|
||||
}
|
||||
|
||||
boolean hideHeader = newCallCallback != null || (newConversationCallback != null && !hasQuery);
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
|
||||
includeSelf,
|
||||
transportType,
|
||||
newCallCallback == null && findByCallback == null,
|
||||
!hideHeader,
|
||||
null,
|
||||
!hideLetterHeaders()
|
||||
!hideLetterHeaders(),
|
||||
newConversationCallback != null ? ContactSearchSortOrder.RECENCY : ContactSearchSortOrder.NATURAL
|
||||
));
|
||||
}
|
||||
|
||||
@@ -944,7 +946,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
builder.username(newRowMode);
|
||||
}
|
||||
|
||||
if (newCallCallback != null || newConversationCallback != null) {
|
||||
if ((newCallCallback != null || newConversationCallback != null) && !hasQuery) {
|
||||
addMoreSection(builder);
|
||||
builder.withEmptyState(emptyBuilder -> {
|
||||
emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms;
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
@@ -28,6 +29,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.qr.kitkat.ScanListener;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.signal.core.util.Base64;
|
||||
@@ -48,6 +50,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
|
||||
private static final String TAG = Log.tag(DeviceActivity.class);
|
||||
|
||||
private static final String EXTRA_DIRECT_TO_SCANNER = "add";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
@@ -56,6 +60,13 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
private DeviceLinkFragment deviceLinkFragment;
|
||||
private MenuItem cameraSwitchItem = null;
|
||||
|
||||
|
||||
public static Intent getIntentForScanner(Context context) {
|
||||
Intent intent = new Intent(context, DeviceActivity.class);
|
||||
intent.putExtra(EXTRA_DIRECT_TO_SCANNER, true);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
@@ -79,7 +90,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
this.deviceListFragment.setAddDeviceButtonListener(this);
|
||||
this.deviceAddFragment.setScanListener(this);
|
||||
|
||||
if (getIntent().getBooleanExtra("add", false)) {
|
||||
if (getIntent().getBooleanExtra(EXTRA_DIRECT_TO_SCANNER, false)) {
|
||||
initFragment(R.id.fragment_container, deviceAddFragment, dynamicLanguage.getCurrentLocale());
|
||||
} else {
|
||||
initFragment(R.id.fragment_container, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
||||
@@ -221,6 +232,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
protected void onPostExecute(Integer result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
LinkedDeviceInactiveCheckJob.enqueue();
|
||||
|
||||
Context context = DeviceActivity.this;
|
||||
|
||||
switch (result) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
@@ -166,6 +167,7 @@ public class DeviceListFragment extends ListFragment
|
||||
super.onPostExecute(result);
|
||||
if (result) {
|
||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||
LinkedDeviceInactiveCheckJob.enqueue();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
|
||||
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
|
||||
.setPositiveButton(R.string.DeviceProvisioningActivity_continue, (dialog1, which) -> {
|
||||
Intent intent = new Intent(DeviceProvisioningActivity.this, DeviceActivity.class);
|
||||
intent.putExtra("add", true);
|
||||
startActivity(intent);
|
||||
startActivity(DeviceActivity.getIntentForScanner(this));
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
|
||||
|
||||
@@ -119,14 +119,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
|
||||
if (Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported()) {
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
smsButton.setOnClickListener(new SmsClickListener());
|
||||
} else {
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
|
||||
private Animation loadAnimation(@AnimRes int animResId) {
|
||||
@@ -200,13 +195,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ViewUtil.animateIn(smsSendFrame, slideInAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsCancelClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
@@ -152,7 +152,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
.setMessage(R.string.OldDeviceTransferLockedDialog__your_signal_account_has_been_transferred_to_your_new_device)
|
||||
.setPositiveButton(R.string.OldDeviceTransferLockedDialog__done, (d, w) -> OldDeviceExitActivity.exit(this))
|
||||
.setNegativeButton(R.string.OldDeviceTransferLockedDialog__cancel_and_activate_this_device, (d, w) -> {
|
||||
SignalStore.misc().clearOldDeviceTransferLocked();
|
||||
SignalStore.misc().setOldDeviceTransferLocked(false);
|
||||
DeviceTransferBlockingInterceptor.getInstance().unblockNetwork();
|
||||
})
|
||||
.setCancelable(false)
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientRepository;
|
||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity;
|
||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
@@ -120,8 +121,6 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
boolean smsSupported = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
@@ -132,33 +131,19 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
|
||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Recipient resolved = Recipient.external(this, number);
|
||||
|
||||
if (!resolved.isRegistered() || !resolved.hasServiceId()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
ContactDiscovery.refresh(this, resolved, false, TimeUnit.SECONDS.toMillis(10));
|
||||
resolved = Recipient.resolved(resolved.getId());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}, resolved -> {
|
||||
SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(this, number), result -> {
|
||||
progress.dismiss();
|
||||
|
||||
if (resolved != null) {
|
||||
if (smsSupported || resolved.isRegistered() && resolved.hasServiceId()) {
|
||||
if (result instanceof RecipientRepository.LookupResult.Success) {
|
||||
Recipient resolved = Recipient.resolved(((RecipientRepository.LookupResult.Success) result).getRecipientId());
|
||||
if (resolved.isRegistered() && resolved.hasServiceId()) {
|
||||
launch(resolved);
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, resolved.getDisplayName(this)))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
} else if (result instanceof RecipientRepository.LookupResult.NotFound || result instanceof RecipientRepository.LookupResult.InvalidEntry) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, number))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
|
||||
@@ -166,8 +151,6 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else if (smsSupported) {
|
||||
launch(Recipient.external(this, number));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +300,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
return null;
|
||||
}
|
||||
|
||||
if (recipient.isRegistered() || (SignalStore.misc().getSmsExportPhase().allowSmsFeatures())) {
|
||||
if (recipient.isRegistered()) {
|
||||
return new ActionItem(
|
||||
R.drawable.ic_phone_right_24,
|
||||
getString(R.string.NewConversationActivity__audio_call),
|
||||
|
||||
@@ -137,6 +137,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
|
||||
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
|
||||
public static final String EXTRA_STARTED_FROM_CALL_LINK = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
|
||||
public static final String EXTRA_LAUNCH_IN_PIP = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
|
||||
|
||||
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
|
||||
private CallStateUpdatePopupWindow callStateUpdatePopupWindow;
|
||||
@@ -159,6 +160,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
private long lastCallLinkDisconnectDialogShowTime;
|
||||
private ControlsAndInfoController controlsAndInfo;
|
||||
private boolean enterPipOnResume;
|
||||
private long lastProcessedIntentTimestamp;
|
||||
|
||||
private Disposable ephemeralStateDisposable = Disposable.empty();
|
||||
|
||||
@@ -264,6 +267,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}, TimeUnit.SECONDS.toMillis(1));
|
||||
}
|
||||
|
||||
if (enterPipOnResume) {
|
||||
enterPipOnResume = false;
|
||||
enterPipModeIfPossible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -299,10 +307,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
requestNewSizesThrottle.clear();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().setEnableVideo(false);
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
if (state != null) {
|
||||
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
} else if (state.getCallState().getInOngoingCall() && isInPipMode()) {
|
||||
ApplicationDependencies.getSignalCallManager().relaunchPipOnForeground();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,6 +375,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
Log.d(TAG, "Intent: Action: " + intent.getAction());
|
||||
Log.d(TAG, "Intent: EXTRA_STARTED_FROM_FULLSCREEN: " + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false));
|
||||
Log.d(TAG, "Intent: EXTRA_ENABLE_VIDEO_IF_AVAILABLE: " + intent.getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false));
|
||||
Log.d(TAG, "Intent: EXTRA_LAUNCH_IN_PIP: " + intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false));
|
||||
}
|
||||
|
||||
private void processIntent(@NonNull Intent intent) {
|
||||
@@ -373,6 +388,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
} else if (END_CALL_ACTION.equals(intent.getAction())) {
|
||||
handleEndCall();
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - lastProcessedIntentTimestamp > TimeUnit.SECONDS.toMillis(1)) {
|
||||
enterPipOnResume = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false);
|
||||
}
|
||||
|
||||
lastProcessedIntentTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private void initializePendingParticipantFragmentListener() {
|
||||
@@ -851,8 +872,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private boolean isSystemPipEnabledAndAvailable() {
|
||||
return Build.VERSION.SDK_INT >= 26 &&
|
||||
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||
return Build.VERSION.SDK_INT >= 26 && getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||
}
|
||||
|
||||
private void delayedFinish() {
|
||||
|
||||
@@ -3,13 +3,14 @@ package org.thoughtcrime.securesms.attachments
|
||||
import android.os.Parcelable
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.util.DatabaseId
|
||||
|
||||
@Parcelize
|
||||
data class AttachmentId(
|
||||
@JsonProperty("rowId")
|
||||
@JvmField
|
||||
val id: Long
|
||||
) : Parcelable {
|
||||
) : Parcelable, DatabaseId {
|
||||
|
||||
val isValid: Boolean
|
||||
get() = id >= 0
|
||||
@@ -17,4 +18,8 @@ data class AttachmentId(
|
||||
override fun toString(): String {
|
||||
return "AttachmentId::$id"
|
||||
}
|
||||
|
||||
override fun serialize(): String {
|
||||
return id.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ class DatabaseAttachment : Attachment {
|
||||
@JvmField
|
||||
val hasData: Boolean
|
||||
|
||||
@JvmField
|
||||
val dataHash: String?
|
||||
|
||||
private val hasThumbnail: Boolean
|
||||
val displayOrder: Int
|
||||
|
||||
@@ -53,7 +56,8 @@ class DatabaseAttachment : Attachment {
|
||||
audioHash: AudioHash?,
|
||||
transformProperties: TransformProperties?,
|
||||
displayOrder: Int,
|
||||
uploadTimestamp: Long
|
||||
uploadTimestamp: Long,
|
||||
dataHash: String?
|
||||
) : super(
|
||||
contentType = contentType!!,
|
||||
transferState = transferProgress,
|
||||
@@ -81,6 +85,7 @@ class DatabaseAttachment : Attachment {
|
||||
this.attachmentId = attachmentId
|
||||
this.mmsId = mmsId
|
||||
this.hasData = hasData
|
||||
this.dataHash = dataHash
|
||||
this.hasThumbnail = hasThumbnail
|
||||
this.displayOrder = displayOrder
|
||||
}
|
||||
@@ -88,6 +93,7 @@ class DatabaseAttachment : Attachment {
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
attachmentId = ParcelCompat.readParcelable(parcel, AttachmentId::class.java.classLoader, AttachmentId::class.java)!!
|
||||
hasData = ParcelUtil.readBoolean(parcel)
|
||||
dataHash = parcel.readString()
|
||||
hasThumbnail = ParcelUtil.readBoolean(parcel)
|
||||
mmsId = parcel.readLong()
|
||||
displayOrder = parcel.readInt()
|
||||
@@ -97,6 +103,7 @@ class DatabaseAttachment : Attachment {
|
||||
super.writeToParcel(dest, flags)
|
||||
dest.writeParcelable(attachmentId, 0)
|
||||
ParcelUtil.writeBoolean(dest, hasData)
|
||||
dest.writeString(dataHash)
|
||||
ParcelUtil.writeBoolean(dest, hasThumbnail)
|
||||
dest.writeLong(mmsId)
|
||||
dest.writeInt(displayOrder)
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.attachments
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.signal.core.util.Base64.encodeWithPadding
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
@@ -14,7 +15,8 @@ import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import java.util.Optional
|
||||
|
||||
class PointerAttachment : Attachment {
|
||||
private constructor(
|
||||
@VisibleForTesting
|
||||
constructor(
|
||||
contentType: String,
|
||||
transferState: Int,
|
||||
size: Long,
|
||||
|
||||
@@ -194,7 +194,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File dataFile = AttachmentTable.newFile(context);
|
||||
File dataFile = AttachmentTable.newDataFile(context);
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
boolean isLegacyTable = SqlUtil.tableExists(db, "part");
|
||||
|
||||
|
||||
@@ -5,10 +5,16 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.EventTimer
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
|
||||
import org.signal.libsignal.messagebackup.MessageBackupKey
|
||||
import org.signal.libsignal.protocol.ServiceId.Aci
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
|
||||
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
|
||||
@@ -16,6 +22,7 @@ import org.thoughtcrime.securesms.backup.v2.processor.CallLogBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
|
||||
@@ -23,12 +30,22 @@ import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveMediaResponse
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
|
||||
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
|
||||
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -36,6 +53,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
object BackupRepository {
|
||||
|
||||
private val TAG = Log.tag(BackupRepository::class.java)
|
||||
private const val VERSION = 1L
|
||||
|
||||
fun export(plaintext: Boolean = false): ByteArray {
|
||||
val eventTimer = EventTimer()
|
||||
@@ -52,7 +70,15 @@ object BackupRepository {
|
||||
)
|
||||
}
|
||||
|
||||
val exportState = ExportState(System.currentTimeMillis())
|
||||
|
||||
writer.use {
|
||||
writer.write(
|
||||
BackupInfo(
|
||||
version = VERSION,
|
||||
backupTimeMs = exportState.backupTime
|
||||
)
|
||||
)
|
||||
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
|
||||
// writes from other threads are blocked. This is something to think more about.
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
@@ -61,12 +87,12 @@ object BackupRepository {
|
||||
eventTimer.emit("account")
|
||||
}
|
||||
|
||||
RecipientBackupProcessor.export {
|
||||
RecipientBackupProcessor.export(exportState) {
|
||||
writer.write(it)
|
||||
eventTimer.emit("recipient")
|
||||
}
|
||||
|
||||
ChatBackupProcessor.export { frame ->
|
||||
ChatBackupProcessor.export(exportState) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("thread")
|
||||
}
|
||||
@@ -76,7 +102,7 @@ object BackupRepository {
|
||||
eventTimer.emit("call")
|
||||
}
|
||||
|
||||
ChatItemBackupProcessor.export { frame ->
|
||||
ChatItemBackupProcessor.export(exportState) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("message")
|
||||
}
|
||||
@@ -88,6 +114,13 @@ object BackupRepository {
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
fun validate(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData): ValidationResult {
|
||||
val masterKey = SignalStore.svr().getOrCreateMasterKey()
|
||||
val key = MessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray()))
|
||||
|
||||
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length)
|
||||
}
|
||||
|
||||
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false) {
|
||||
val eventTimer = EventTimer()
|
||||
|
||||
@@ -102,6 +135,15 @@ object BackupRepository {
|
||||
)
|
||||
}
|
||||
|
||||
val header = frameReader.getHeader()
|
||||
if (header == null) {
|
||||
Log.e(TAG, "Backup is missing header!")
|
||||
return
|
||||
} else if (header.version > VERSION) {
|
||||
Log.e(TAG, "Backup version is newer than we understand: ${header.version}")
|
||||
return
|
||||
}
|
||||
|
||||
// Note: Without a transaction, bad imports could lead to lost data. But because we have a transaction,
|
||||
// writes from other threads are blocked. This is something to think more about.
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
@@ -117,6 +159,7 @@ object BackupRepository {
|
||||
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
|
||||
SignalDatabase.recipients.setProfileSharing(selfId, true)
|
||||
|
||||
eventTimer.emit("setup")
|
||||
val backupState = BackupState()
|
||||
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
|
||||
|
||||
@@ -161,6 +204,14 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
val groups = SignalDatabase.groups.getGroups()
|
||||
while (groups.hasNext()) {
|
||||
val group = groups.next()
|
||||
if (group.id.isV2) {
|
||||
ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(group.id as GroupId.V2))
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "import() ${eventTimer.stop().summary}")
|
||||
}
|
||||
|
||||
@@ -230,6 +281,77 @@ object BackupRepository {
|
||||
.also { Log.i(TAG, "OverallResult: $it") } is NetworkResult.Success
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with details about the remote backup state.
|
||||
*/
|
||||
fun debugGetArchivedMediaState(): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return api
|
||||
.triggerBackupIdReservation(backupKey)
|
||||
.then { getAuthCredential() }
|
||||
.then { credential ->
|
||||
api.debugGetUploadedMediaItemMetadata(backupKey, credential)
|
||||
}
|
||||
}
|
||||
|
||||
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return api
|
||||
.triggerBackupIdReservation(backupKey)
|
||||
.then { getAuthCredential() }
|
||||
.then { credential ->
|
||||
api.archiveAttachmentMedia(
|
||||
backupKey = backupKey,
|
||||
serviceCredential = credential,
|
||||
item = attachment.toArchiveMediaRequest(backupKey)
|
||||
)
|
||||
}
|
||||
.also { Log.i(TAG, "backupMediaResult: $it") }
|
||||
}
|
||||
|
||||
fun archiveMedia(attachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResponse> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return api
|
||||
.triggerBackupIdReservation(backupKey)
|
||||
.then { getAuthCredential() }
|
||||
.then { credential ->
|
||||
api.archiveAttachmentMedia(
|
||||
backupKey = backupKey,
|
||||
serviceCredential = credential,
|
||||
items = attachments.map { it.toArchiveMediaRequest(backupKey) }
|
||||
)
|
||||
}
|
||||
.also { Log.i(TAG, "backupMediaResult: $it") }
|
||||
}
|
||||
|
||||
fun deleteArchivedMedia(attachments: List<DatabaseAttachment>): NetworkResult<Unit> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
val mediaToDelete = attachments.map {
|
||||
DeleteArchivedMediaRequest.ArchivedMediaObject(
|
||||
cdn = 3, // TODO [cody] store and reuse backup cdn returned from copy/move call
|
||||
mediaId = backupKey.deriveMediaId(Base64.decode(it.dataHash!!)).toString()
|
||||
)
|
||||
}
|
||||
|
||||
return getAuthCredential()
|
||||
.then { credential ->
|
||||
api.deleteArchivedMedia(
|
||||
backupKey = backupKey,
|
||||
serviceCredential = credential,
|
||||
mediaToDelete = mediaToDelete
|
||||
)
|
||||
}
|
||||
.also { Log.i(TAG, "deleteBackupMediaResult: $it") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an auth credential, preferring a cached value if available.
|
||||
*/
|
||||
@@ -257,6 +379,26 @@ object BackupRepository {
|
||||
val e164: String,
|
||||
val profileKey: ProfileKey
|
||||
)
|
||||
|
||||
private fun DatabaseAttachment.toArchiveMediaRequest(backupKey: BackupKey): ArchiveMediaRequest {
|
||||
val mediaSecrets = backupKey.deriveMediaSecrets(Base64.decode(dataHash!!))
|
||||
return ArchiveMediaRequest(
|
||||
sourceAttachment = ArchiveMediaRequest.SourceAttachment(
|
||||
cdn = cdnNumber,
|
||||
key = remoteLocation!!
|
||||
),
|
||||
objectLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(size)).toInt(),
|
||||
mediaId = mediaSecrets.id.toString(),
|
||||
hmacKey = Base64.encodeWithPadding(mediaSecrets.macKey),
|
||||
encryptionKey = Base64.encodeWithPadding(mediaSecrets.cipherKey),
|
||||
iv = Base64.encodeWithPadding(mediaSecrets.iv)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ExportState(val backupTime: Long) {
|
||||
val recipientIds = HashSet<Long>()
|
||||
val threadIds = HashSet<Long>()
|
||||
}
|
||||
|
||||
class BackupState {
|
||||
|
||||
@@ -39,17 +39,12 @@ fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupStat
|
||||
Call.Type.UNKNOWN_TYPE -> return
|
||||
}
|
||||
|
||||
val event = when (call.event) {
|
||||
Call.Event.DELETE -> CallTable.Event.DELETE
|
||||
Call.Event.JOINED -> CallTable.Event.JOINED
|
||||
Call.Event.GENERIC_GROUP_CALL -> CallTable.Event.GENERIC_GROUP_CALL
|
||||
Call.Event.DECLINED -> CallTable.Event.DECLINED
|
||||
Call.Event.ACCEPTED -> CallTable.Event.ACCEPTED
|
||||
Call.Event.MISSED -> CallTable.Event.MISSED
|
||||
Call.Event.OUTGOING_RING -> CallTable.Event.OUTGOING_RING
|
||||
Call.Event.OUTGOING -> CallTable.Event.ONGOING
|
||||
Call.Event.NOT_ACCEPTED -> CallTable.Event.NOT_ACCEPTED
|
||||
Call.Event.UNKNOWN_EVENT -> return
|
||||
val event = when (call.state) {
|
||||
Call.State.MISSED -> CallTable.Event.MISSED
|
||||
Call.State.COMPLETED -> CallTable.Event.ACCEPTED
|
||||
Call.State.DECLINED_BY_USER -> CallTable.Event.DECLINED
|
||||
Call.State.DECLINED_BY_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE
|
||||
Call.State.UNKNOWN_EVENT -> return
|
||||
}
|
||||
|
||||
val direction = if (call.outgoing) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING
|
||||
@@ -62,7 +57,8 @@ fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupStat
|
||||
CallTable.TYPE to CallTable.Type.serialize(type),
|
||||
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
|
||||
CallTable.EVENT to CallTable.Event.serialize(event),
|
||||
CallTable.TIMESTAMP to call.timestamp
|
||||
CallTable.TIMESTAMP to call.timestamp,
|
||||
CallTable.RINGER to if (call.ringerRecipientId != null) backupState.backupToLocalRecipientId[call.ringerRecipientId]?.toLong() else null
|
||||
)
|
||||
|
||||
writableDatabase.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
|
||||
@@ -102,18 +98,18 @@ class CallLogIterator(private val cursor: Cursor) : Iterator<BackupCall?>, Close
|
||||
},
|
||||
timestamp = cursor.requireLong(CallTable.TIMESTAMP),
|
||||
ringerRecipientId = if (cursor.isNull(CallTable.RINGER)) null else cursor.requireLong(CallTable.RINGER),
|
||||
event = when (event) {
|
||||
CallTable.Event.ONGOING -> Call.Event.OUTGOING
|
||||
CallTable.Event.OUTGOING_RING -> Call.Event.OUTGOING_RING
|
||||
CallTable.Event.ACCEPTED -> Call.Event.ACCEPTED
|
||||
CallTable.Event.DECLINED -> Call.Event.DECLINED
|
||||
CallTable.Event.GENERIC_GROUP_CALL -> Call.Event.GENERIC_GROUP_CALL
|
||||
CallTable.Event.JOINED -> Call.Event.JOINED
|
||||
CallTable.Event.MISSED,
|
||||
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> Call.Event.MISSED
|
||||
CallTable.Event.DELETE -> Call.Event.DELETE
|
||||
CallTable.Event.RINGING -> Call.Event.UNKNOWN_EVENT
|
||||
CallTable.Event.NOT_ACCEPTED -> Call.Event.NOT_ACCEPTED
|
||||
state = when (event) {
|
||||
CallTable.Event.ONGOING -> Call.State.COMPLETED
|
||||
CallTable.Event.OUTGOING_RING -> Call.State.COMPLETED
|
||||
CallTable.Event.ACCEPTED -> Call.State.COMPLETED
|
||||
CallTable.Event.DECLINED -> Call.State.DECLINED_BY_USER
|
||||
CallTable.Event.GENERIC_GROUP_CALL -> Call.State.COMPLETED
|
||||
CallTable.Event.JOINED -> Call.State.COMPLETED
|
||||
CallTable.Event.MISSED -> Call.State.MISSED
|
||||
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> Call.State.DECLINED_BY_NOTIFICATION_PROFILE
|
||||
CallTable.Event.DELETE -> Call.State.COMPLETED
|
||||
CallTable.Event.RINGING -> Call.State.MISSED
|
||||
CallTable.Event.NOT_ACCEPTED -> Call.State.MISSED
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.database.Cursor
|
||||
import com.annimon.stream.Stream
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Base64.decode
|
||||
import org.signal.core.util.Base64.decodeOrThrow
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireBlob
|
||||
@@ -16,12 +17,15 @@ import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||
@@ -40,11 +44,16 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
|
||||
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
|
||||
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
@@ -99,6 +108,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
|
||||
val reactionsById: Map<Long, List<ReactionRecord>> = SignalDatabase.reactions.getReactionsForMessages(records.keys)
|
||||
val mentionsById: Map<Long, List<Mention>> = SignalDatabase.mentions.getMentionsForMessages(records.keys)
|
||||
val attachmentsById: Map<Long, List<DatabaseAttachment>> = SignalDatabase.attachments.getAttachmentsForMessages(records.keys)
|
||||
val groupReceiptsById: Map<Long, List<GroupReceiptTable.GroupReceiptInfo>> = SignalDatabase.groupReceipts.getGroupReceiptInfoForMessages(records.keys)
|
||||
|
||||
for ((id, record) in records) {
|
||||
@@ -110,14 +121,23 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
MessageTypes.isIdentityUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_UPDATE))
|
||||
MessageTypes.isIdentityVerified(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_VERIFIED))
|
||||
MessageTypes.isIdentityDefault(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_DEFAULT))
|
||||
MessageTypes.isChangeNumber(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
|
||||
MessageTypes.isBoostRequest(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
|
||||
MessageTypes.isChangeNumber(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
|
||||
builder.sms = false
|
||||
}
|
||||
MessageTypes.isBoostRequest(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
|
||||
builder.sms = false
|
||||
}
|
||||
MessageTypes.isEndSessionType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.END_SESSION))
|
||||
MessageTypes.isChatSessionRefresh(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHAT_SESSION_REFRESH))
|
||||
MessageTypes.isBadDecryptType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BAD_DECRYPT))
|
||||
MessageTypes.isPaymentsActivated(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENTS_ACTIVATED))
|
||||
MessageTypes.isPaymentsRequestToActivate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST))
|
||||
MessageTypes.isExpirationTimerUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate((record.expiresIn / 1000).toInt()))
|
||||
MessageTypes.isExpirationTimerUpdate(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn.toInt()))
|
||||
builder.expiresInMs = null
|
||||
}
|
||||
MessageTypes.isProfileChange(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
profileChange = try {
|
||||
@@ -133,6 +153,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
ProfileChangeChatUpdate()
|
||||
}
|
||||
)
|
||||
builder.sms = false
|
||||
}
|
||||
MessageTypes.isSessionSwitchoverType(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
@@ -154,6 +175,26 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
)
|
||||
}
|
||||
MessageTypes.isGroupV2(record.type) && MessageTypes.isGroupUpdate(record.type) -> {
|
||||
val groupChange = record.messageExtras?.gv2UpdateDescription?.groupChangeUpdate
|
||||
if (groupChange != null) {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
groupChange = groupChange
|
||||
)
|
||||
} else if (record.body != null) {
|
||||
try {
|
||||
val decoded: ByteArray = decode(record.body)
|
||||
val context = DecryptedGroupV2Context.ADAPTER.decode(decoded)
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
groupChange = GroupsV2UpdateMessageConverter.translateDecryptedChange(selfIds = SignalStore.account().getServiceIds(), context)
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
MessageTypes.isCallLog(record.type) -> {
|
||||
val call = calls.getCallByMessageId(record.id)
|
||||
if (call != null) {
|
||||
@@ -161,10 +202,10 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
} else {
|
||||
when {
|
||||
MessageTypes.isMissedAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_AUDIO_CALL)))
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL)))
|
||||
}
|
||||
MessageTypes.isMissedVideoCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_VIDEO_CALL)))
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL)))
|
||||
}
|
||||
MessageTypes.isIncomingAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL)))
|
||||
@@ -203,11 +244,11 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
}
|
||||
}
|
||||
record.body == null -> {
|
||||
Log.w(TAG, "Record missing a body, skipping")
|
||||
record.body == null && !attachmentsById.containsKey(record.id) -> {
|
||||
Log.w(TAG, "Record missing a body and doesnt have attachments, skipping")
|
||||
continue
|
||||
}
|
||||
else -> builder.standardMessage = record.toTextMessage(reactionsById[id])
|
||||
else -> builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
|
||||
}
|
||||
|
||||
buffer += builder.build()
|
||||
@@ -241,7 +282,6 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
chatId = record.threadId
|
||||
authorId = record.fromRecipientId
|
||||
dateSent = record.dateSent
|
||||
sealedSender = record.sealedSender
|
||||
expireStartDate = if (record.expireStarted > 0) record.expireStarted else null
|
||||
expiresInMs = if (record.expiresIn > 0) record.expiresIn else null
|
||||
revisions = emptyList()
|
||||
@@ -255,19 +295,28 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
incoming = ChatItem.IncomingMessageDetails(
|
||||
dateServerSent = record.dateServer,
|
||||
dateReceived = record.dateReceived,
|
||||
read = record.read
|
||||
read = record.read,
|
||||
sealedSender = record.sealedSender
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toTextMessage(reactionRecords: List<ReactionRecord>?): StandardMessage {
|
||||
private fun BackupMessageRecord.toStandardMessage(reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?, attachments: List<DatabaseAttachment>?): StandardMessage {
|
||||
val text = if (body == null) {
|
||||
null
|
||||
} else {
|
||||
Text(
|
||||
body = this.body,
|
||||
bodyRanges = (this.bodyRanges?.toBackupBodyRanges() ?: emptyList()) + (mentions?.toBackupBodyRanges() ?: emptyList())
|
||||
)
|
||||
}
|
||||
val quotedAttachments = attachments?.filter { it.quote } ?: emptyList()
|
||||
val messageAttachments = attachments?.filter { !it.quote } ?: emptyList()
|
||||
return StandardMessage(
|
||||
quote = this.toQuote(),
|
||||
text = Text(
|
||||
body = this.body!!,
|
||||
bodyRanges = this.bodyRanges?.toBackupBodyRanges() ?: emptyList()
|
||||
),
|
||||
quote = this.toQuote(quotedAttachments),
|
||||
text = text,
|
||||
attachments = messageAttachments.toBackupAttachments(),
|
||||
// TODO Link previews!
|
||||
linkPreview = emptyList(),
|
||||
longText = null,
|
||||
@@ -275,14 +324,14 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
)
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toQuote(): Quote? {
|
||||
private fun BackupMessageRecord.toQuote(attachments: List<DatabaseAttachment>? = null): Quote? {
|
||||
return if (this.quoteTargetSentTimestamp != MessageTable.QUOTE_NOT_PRESENT_ID && this.quoteAuthor > 0) {
|
||||
// TODO Attachments!
|
||||
val type = QuoteModel.Type.fromCode(this.quoteType)
|
||||
Quote(
|
||||
targetSentTimestamp = this.quoteTargetSentTimestamp.takeIf { !this.quoteMissing && it != MessageTable.QUOTE_TARGET_MISSING_ID },
|
||||
authorId = this.quoteAuthor,
|
||||
text = this.quoteBody,
|
||||
attachments = attachments?.toBackupQuoteAttachments() ?: emptyList(),
|
||||
bodyRanges = this.quoteBodyRanges?.toBackupBodyRanges() ?: emptyList(),
|
||||
type = when (type) {
|
||||
QuoteModel.Type.NORMAL -> Quote.Type.NORMAL
|
||||
@@ -294,6 +343,54 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<DatabaseAttachment>.toBackupQuoteAttachments(): List<Quote.QuotedAttachment> {
|
||||
return this.map { attachment ->
|
||||
Quote.QuotedAttachment(
|
||||
contentType = attachment.contentType,
|
||||
fileName = attachment.fileName,
|
||||
thumbnail = attachment.toBackupAttachment()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.toBackupAttachment(): MessageAttachment {
|
||||
return MessageAttachment(
|
||||
pointer = FilePointer(
|
||||
attachmentLocator = FilePointer.AttachmentLocator(
|
||||
cdnKey = this.remoteLocation ?: "",
|
||||
cdnNumber = this.cdnNumber,
|
||||
uploadTimestamp = this.uploadTimestamp
|
||||
),
|
||||
key = if (remoteKey != null) decode(remoteKey).toByteString() else null,
|
||||
contentType = this.contentType,
|
||||
size = this.size.toInt(),
|
||||
incrementalMac = this.incrementalDigest?.toByteString(),
|
||||
incrementalMacChunkSize = this.incrementalMacChunkSize,
|
||||
fileName = this.fileName,
|
||||
width = this.width,
|
||||
height = this.height,
|
||||
caption = this.caption,
|
||||
blurHash = this.blurHash?.hash
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<DatabaseAttachment>.toBackupAttachments(): List<MessageAttachment> {
|
||||
return this.map { attachment ->
|
||||
attachment.toBackupAttachment()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Mention>.toBackupBodyRanges(): List<BackupBodyRange> {
|
||||
return this.map {
|
||||
BackupBodyRange(
|
||||
start = it.start,
|
||||
length = it.length,
|
||||
mentionAci = SignalDatabase.recipients.getRecord(it.recipientId).aci?.toByteString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.toBackupBodyRanges(): List<BackupBodyRange> {
|
||||
val decoded: BodyRangeList = try {
|
||||
BodyRangeList.ADAPTER.decode(this)
|
||||
@@ -306,7 +403,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
BackupBodyRange(
|
||||
start = it.start,
|
||||
length = it.length,
|
||||
mentionAci = it.mentionUuid?.let { UuidUtil.parseOrThrow(it) }?.toByteArray()?.toByteString(),
|
||||
mentionAci = it.mentionUuid?.let { uuid -> UuidUtil.parseOrThrow(uuid) }?.toByteArray()?.toByteString(),
|
||||
style = it.style?.toBackupBodyRangeStyle()
|
||||
)
|
||||
}
|
||||
@@ -412,6 +509,17 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray?.parseMessageExtras(): MessageExtras? {
|
||||
if (this == null) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
MessageExtras.ADAPTER.decode(this)
|
||||
} catch (e: java.lang.Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor.toBackupMessageRecord(): BackupMessageRecord {
|
||||
return BackupMessageRecord(
|
||||
id = this.requireLong(MessageTable.ID),
|
||||
@@ -443,7 +551,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
receiptTimestamp = this.requireLong(MessageTable.RECEIPT_TIMESTAMP),
|
||||
networkFailureRecipientIds = this.requireString(MessageTable.NETWORK_FAILURES).parseNetworkFailures(),
|
||||
identityMismatchRecipientIds = this.requireString(MessageTable.MISMATCHED_IDENTITIES).parseIdentityMismatches(),
|
||||
baseType = this.requireLong(COLUMN_BASE_TYPE)
|
||||
baseType = this.requireLong(COLUMN_BASE_TYPE),
|
||||
messageExtras = this.requireBlob(MessageTable.MESSAGE_EXTRAS).parseMessageExtras()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -477,6 +586,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
val read: Boolean,
|
||||
val networkFailureRecipientIds: Set<Long>,
|
||||
val identityMismatchRecipientIds: Set<Long>,
|
||||
val baseType: Long
|
||||
val baseType: Long,
|
||||
val messageExtras: MessageExtras?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,13 +10,17 @@ import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.toInt
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
||||
@@ -28,11 +32,15 @@ import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.ReactionTable
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||
@@ -40,7 +48,12 @@ import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* An object that will ingest all fo the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
|
||||
@@ -152,7 +165,6 @@ class ChatItemImportInserter(
|
||||
if (buffer.size == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
buildBulkInsert(MessageTable.TABLE_NAME, MESSAGE_COLUMNS, buffer.messages).forEach {
|
||||
db.rawQuery("${it.query.where} RETURNING ${MessageTable.ID}", it.query.whereArgs).use { cursor ->
|
||||
var index = 0
|
||||
@@ -177,6 +189,8 @@ class ChatItemImportInserter(
|
||||
|
||||
messageId = SqlUtil.getNextAutoIncrementId(db, MessageTable.TABLE_NAME)
|
||||
|
||||
buffer.reset()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -202,6 +216,38 @@ class ChatItemImportInserter(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.standardMessage != null) {
|
||||
val bodyRanges = this.standardMessage.text?.bodyRanges
|
||||
if (!bodyRanges.isNullOrEmpty()) {
|
||||
val mentions = bodyRanges.filter { it.mentionAci != null && it.start != null && it.length != null }
|
||||
.mapNotNull {
|
||||
val aci = ServiceId.ACI.parseOrNull(it.mentionAci!!)
|
||||
|
||||
if (aci != null && !aci.isUnknown) {
|
||||
val id = RecipientId.from(aci)
|
||||
Mention(id, it.start!!, it.length!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (mentions.isNotEmpty()) {
|
||||
followUp = { messageId ->
|
||||
SignalDatabase.mentions.insert(threadId, messageId, mentions)
|
||||
}
|
||||
}
|
||||
}
|
||||
val attachments = this.standardMessage.attachments.mapNotNull { attachment ->
|
||||
attachment.toLocalAttachment()
|
||||
}
|
||||
val quoteAttachments = this.standardMessage.quote?.attachments?.mapNotNull {
|
||||
it.toLocalAttachment()
|
||||
} ?: emptyList()
|
||||
if (attachments.isNotEmpty()) {
|
||||
followUp = { messageRowId ->
|
||||
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, attachments, quoteAttachments)
|
||||
}
|
||||
}
|
||||
}
|
||||
return MessageInsert(contentValues, followUp)
|
||||
}
|
||||
|
||||
@@ -217,7 +263,7 @@ class ChatItemImportInserter(
|
||||
contentValues.put(MessageTable.TO_RECIPIENT_ID, (if (this.outgoing != null) chatRecipientId else selfId).serialize())
|
||||
contentValues.put(MessageTable.THREAD_ID, threadId)
|
||||
contentValues.put(MessageTable.DATE_RECEIVED, this.incoming?.dateReceived ?: this.dateSent)
|
||||
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOf { it.lastStatusUpdateTimestamp } ?: 0)
|
||||
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOfOrNull { it.lastStatusUpdateTimestamp } ?: 0)
|
||||
contentValues.putNull(MessageTable.LATEST_REVISION_ID)
|
||||
contentValues.putNull(MessageTable.ORIGINAL_MESSAGE_ID)
|
||||
contentValues.put(MessageTable.REVISION_NUMBER, 0)
|
||||
@@ -241,8 +287,9 @@ class ChatItemImportInserter(
|
||||
contentValues.put(MessageTable.VIEWED_COLUMN, 0)
|
||||
contentValues.put(MessageTable.HAS_READ_RECEIPT, 0)
|
||||
contentValues.put(MessageTable.HAS_DELIVERY_RECEIPT, 0)
|
||||
contentValues.put(MessageTable.UNIDENTIFIED, this.sealedSender?.toInt())
|
||||
contentValues.put(MessageTable.UNIDENTIFIED, this.incoming?.sealedSender?.toInt() ?: 0)
|
||||
contentValues.put(MessageTable.READ, this.incoming?.read?.toInt() ?: 0)
|
||||
contentValues.put(MessageTable.NOTIFIED, 1)
|
||||
}
|
||||
|
||||
contentValues.put(MessageTable.QUOTE_ID, 0)
|
||||
@@ -265,7 +312,6 @@ class ChatItemImportInserter(
|
||||
val reactions: List<Reaction> = when {
|
||||
this.standardMessage != null -> this.standardMessage.reactions
|
||||
this.contactMessage != null -> this.contactMessage.reactions
|
||||
this.voiceMessage != null -> this.voiceMessage.reactions
|
||||
this.stickerMessage != null -> this.stickerMessage.reactions
|
||||
else -> emptyList()
|
||||
}
|
||||
@@ -342,7 +388,7 @@ class ChatItemImportInserter(
|
||||
this.put(MessageTable.BODY, standardMessage.text.body)
|
||||
|
||||
if (standardMessage.text.bodyRanges.isNotEmpty()) {
|
||||
this.put(MessageTable.MESSAGE_RANGES, standardMessage.text.bodyRanges.toLocalBodyRanges()?.encode() as ByteArray?)
|
||||
this.put(MessageTable.MESSAGE_RANGES, standardMessage.text.bodyRanges.toLocalBodyRanges()?.encode())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,23 +401,24 @@ class ChatItemImportInserter(
|
||||
var typeFlags: Long = 0
|
||||
when {
|
||||
updateMessage.simpleUpdate != null -> {
|
||||
val typeWithoutBase = (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
|
||||
typeFlags = when (updateMessage.simpleUpdate.type) {
|
||||
SimpleChatUpdate.Type.UNKNOWN -> 0
|
||||
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE
|
||||
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT
|
||||
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT
|
||||
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT
|
||||
SimpleChatUpdate.Type.UNKNOWN -> typeWithoutBase
|
||||
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.CHANGE_NUMBER -> MessageTypes.CHANGE_NUMBER_TYPE
|
||||
SimpleChatUpdate.Type.BOOST_REQUEST -> MessageTypes.BOOST_REQUEST_TYPE
|
||||
SimpleChatUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT
|
||||
SimpleChatUpdate.Type.CHAT_SESSION_REFRESH -> MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT
|
||||
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE
|
||||
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED
|
||||
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST
|
||||
SimpleChatUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.CHAT_SESSION_REFRESH -> MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE or typeWithoutBase
|
||||
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED or typeWithoutBase
|
||||
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST or typeWithoutBase
|
||||
}
|
||||
}
|
||||
updateMessage.expirationTimerChange != null -> {
|
||||
typeFlags = MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
|
||||
typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
|
||||
put(MessageTable.EXPIRES_IN, updateMessage.expirationTimerChange.expiresInMs.toLong())
|
||||
}
|
||||
updateMessage.profileChange != null -> {
|
||||
@@ -381,12 +428,12 @@ class ChatItemImportInserter(
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(profileChangeDetails))
|
||||
}
|
||||
updateMessage.sessionSwitchover != null -> {
|
||||
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE
|
||||
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
|
||||
val sessionSwitchoverDetails = SessionSwitchoverEvent(e164 = updateMessage.sessionSwitchover.e164.toString()).encode()
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(sessionSwitchoverDetails))
|
||||
}
|
||||
updateMessage.threadMerge != null -> {
|
||||
typeFlags = MessageTypes.THREAD_MERGE_TYPE
|
||||
typeFlags = MessageTypes.THREAD_MERGE_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
|
||||
val threadMergeDetails = ThreadMergeEvent(previousE164 = updateMessage.threadMerge.previousE164.toString()).encode()
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(threadMergeDetails))
|
||||
}
|
||||
@@ -401,8 +448,10 @@ class ChatItemImportInserter(
|
||||
IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL -> MessageTypes.INCOMING_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
|
||||
}
|
||||
}
|
||||
@@ -410,8 +459,19 @@ class ChatItemImportInserter(
|
||||
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here
|
||||
this.put(MessageTable.TYPE, typeFlags)
|
||||
}
|
||||
updateMessage.groupChange != null -> {
|
||||
put(MessageTable.BODY, "")
|
||||
put(
|
||||
MessageTable.MESSAGE_EXTRAS,
|
||||
MessageExtras(
|
||||
gv2UpdateDescription =
|
||||
GV2UpdateDescription(groupChangeUpdate = updateMessage.groupChange)
|
||||
).encode()
|
||||
)
|
||||
typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT
|
||||
}
|
||||
}
|
||||
this.put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or typeFlags)
|
||||
this.put(MessageTable.TYPE, typeFlags)
|
||||
}
|
||||
|
||||
private fun ContentValues.addQuote(quote: Quote) {
|
||||
@@ -470,7 +530,7 @@ class ChatItemImportInserter(
|
||||
}
|
||||
|
||||
return BodyRangeList(
|
||||
ranges = this.map { bodyRange ->
|
||||
ranges = this.filter { it.mentionAci == null }.map { bodyRange ->
|
||||
BodyRangeList.BodyRange(
|
||||
mentionUuid = bodyRange.mentionAci?.let { UuidUtil.fromByteString(it) }?.toString(),
|
||||
style = bodyRange.style?.let {
|
||||
@@ -503,6 +563,39 @@ class ChatItemImportInserter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageAttachment.toLocalAttachment(contentType: String? = pointer?.contentType, fileName: String? = pointer?.fileName): Attachment? {
|
||||
if (pointer == null) return null
|
||||
if (pointer.attachmentLocator != null) {
|
||||
val signalAttachmentPointer = SignalServiceAttachmentPointer(
|
||||
pointer.attachmentLocator.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey),
|
||||
contentType,
|
||||
pointer.key?.toByteArray(),
|
||||
Optional.ofNullable(pointer.size),
|
||||
Optional.empty(),
|
||||
pointer.width ?: 0,
|
||||
pointer.height ?: 0,
|
||||
Optional.empty(),
|
||||
Optional.ofNullable(pointer.incrementalMac?.toByteArray()),
|
||||
pointer.incrementalMacChunkSize ?: 0,
|
||||
Optional.ofNullable(fileName),
|
||||
flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
flag == MessageAttachment.Flag.BORDERLESS,
|
||||
flag == MessageAttachment.Flag.GIF,
|
||||
Optional.ofNullable(pointer.caption),
|
||||
Optional.ofNullable(pointer.blurHash),
|
||||
pointer.attachmentLocator.uploadTimestamp
|
||||
)
|
||||
return PointerAttachment.forPointer(Optional.of(signalAttachmentPointer)).orNull()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
|
||||
return thumbnail?.toLocalAttachment(this.contentType, this.fileName)
|
||||
?: if (this.contentType == null) null else PointerAttachment.forPointer(SignalServiceDataMessage.Quote.QuotedAttachment(contentType = this.contentType!!, fileName = this.fileName, thumbnail = null)).orNull()
|
||||
}
|
||||
|
||||
private class MessageInsert(val contentValues: ContentValues, val followUp: ((Long) -> Unit)?)
|
||||
|
||||
private class Buffer(
|
||||
@@ -512,5 +605,11 @@ class ChatItemImportInserter(
|
||||
) {
|
||||
val size: Int
|
||||
get() = listOf(messages.size, reactions.size, groupReceipts.size).max()
|
||||
|
||||
fun reset() {
|
||||
messages.clear()
|
||||
reactions.clear()
|
||||
groupReceipts.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,38 +28,45 @@ import org.thoughtcrime.securesms.backup.v2.proto.DistributionList as BackupDist
|
||||
|
||||
private val TAG = Log.tag(DistributionListTables::class.java)
|
||||
|
||||
data class DistributionRecipient(val id: RecipientId, val record: DistributionListRecord)
|
||||
|
||||
fun DistributionListTables.getAllForBackup(): List<BackupRecipient> {
|
||||
val records = readableDatabase
|
||||
.select()
|
||||
.from(DistributionListTables.ListTable.TABLE_NAME)
|
||||
.where(DistributionListTables.ListTable.IS_NOT_DELETED)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val id: DistributionListId = DistributionListId.from(cursor.requireLong(DistributionListTables.ListTable.ID))
|
||||
val privacyMode: DistributionListPrivacyMode = cursor.requireObject(DistributionListTables.ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
|
||||
DistributionListRecord(
|
||||
id = id,
|
||||
name = cursor.requireNonNullString(DistributionListTables.ListTable.NAME),
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(DistributionListTables.ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, DistributionListTables.ListTable.ALLOWS_REPLIES),
|
||||
rawMembers = getRawMembers(id, privacyMode),
|
||||
members = getMembers(id),
|
||||
deletedAtTimestamp = 0L,
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, DistributionListTables.ListTable.IS_UNKNOWN),
|
||||
privacyMode = privacyMode
|
||||
val recipientId: RecipientId = RecipientId.from(cursor.requireLong(DistributionListTables.ListTable.RECIPIENT_ID))
|
||||
DistributionRecipient(
|
||||
id = recipientId,
|
||||
record = DistributionListRecord(
|
||||
id = id,
|
||||
name = cursor.requireNonNullString(DistributionListTables.ListTable.NAME),
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(DistributionListTables.ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, DistributionListTables.ListTable.ALLOWS_REPLIES),
|
||||
rawMembers = getRawMembers(id, privacyMode),
|
||||
members = getMembers(id),
|
||||
deletedAtTimestamp = 0L,
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, DistributionListTables.ListTable.IS_UNKNOWN),
|
||||
privacyMode = privacyMode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return records
|
||||
.map { record ->
|
||||
.map { recipient ->
|
||||
BackupRecipient(
|
||||
id = recipient.id.toLong(),
|
||||
distributionList = BackupDistributionList(
|
||||
name = record.name,
|
||||
distributionId = record.distributionId.asUuid().toByteArray().toByteString(),
|
||||
allowReplies = record.allowsReplies,
|
||||
deletionTimestamp = record.deletedAtTimestamp,
|
||||
privacyMode = record.privacyMode.toBackupPrivacyMode(),
|
||||
memberRecipientIds = record.members.map { it.toLong() }
|
||||
name = recipient.record.name,
|
||||
distributionId = recipient.record.distributionId.asUuid().toByteArray().toByteString(),
|
||||
allowReplies = recipient.record.allowsReplies,
|
||||
deletionTimestamp = recipient.record.deletedAtTimestamp,
|
||||
privacyMode = recipient.record.privacyMode.toBackupPrivacyMode(),
|
||||
memberRecipientIds = recipient.record.members.map { it.toLong() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import org.signal.core.util.select
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private val TAG = Log.tag(MessageTable::class.java)
|
||||
private const val BASE_TYPE = "base_type"
|
||||
|
||||
fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
|
||||
fun MessageTable.getMessagesForBackup(backupTime: Long): ChatItemExportIterator {
|
||||
val cursor = readableDatabase
|
||||
.select(
|
||||
MessageTable.ID,
|
||||
@@ -47,18 +48,17 @@ fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
|
||||
MessageTable.READ,
|
||||
MessageTable.NETWORK_FAILURES,
|
||||
MessageTable.MISMATCHED_IDENTITIES,
|
||||
"${MessageTable.TYPE} & ${MessageTypes.BASE_TYPE_MASK} AS ${ChatItemExportIterator.COLUMN_BASE_TYPE}"
|
||||
"${MessageTable.TYPE} & ${MessageTypes.BASE_TYPE_MASK} AS ${ChatItemExportIterator.COLUMN_BASE_TYPE}",
|
||||
MessageTable.MESSAGE_EXTRAS
|
||||
)
|
||||
.from(MessageTable.TABLE_NAME)
|
||||
.where(
|
||||
"""
|
||||
$BASE_TYPE IN (
|
||||
${MessageTypes.BASE_INBOX_TYPE},
|
||||
${MessageTypes.BASE_OUTBOX_TYPE},
|
||||
${MessageTypes.BASE_SENT_TYPE},
|
||||
${MessageTypes.BASE_SENDING_TYPE},
|
||||
${MessageTypes.BASE_SENT_FAILED_TYPE}
|
||||
) OR ${MessageTable.IS_CALL_TYPE_CLAUSE}
|
||||
(
|
||||
${MessageTable.EXPIRE_STARTED} = 0
|
||||
OR
|
||||
(${MessageTable.EXPIRES_IN} > 0 AND (${MessageTable.EXPIRE_STARTED} + ${MessageTable.EXPIRES_IN}) > $backupTime + ${TimeUnit.DAYS.toMillis(1)})
|
||||
)
|
||||
"""
|
||||
)
|
||||
.orderBy("${MessageTable.DATE_RECEIVED} ASC")
|
||||
|
||||
@@ -22,23 +22,31 @@ import org.signal.core.util.select
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.core.util.update
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Contact
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Group
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Self
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.RecipientTableCursorUtil
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.util.toByteArray
|
||||
import java.io.Closeable
|
||||
|
||||
typealias BackupRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
|
||||
@@ -94,7 +102,8 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator {
|
||||
"${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL}",
|
||||
"${RecipientTable.TABLE_NAME}.${RecipientTable.EXTRAS}",
|
||||
"${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY}",
|
||||
"${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}"
|
||||
"${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}",
|
||||
"${GroupTable.TABLE_NAME}.${GroupTable.TITLE}"
|
||||
)
|
||||
.from(
|
||||
"""
|
||||
@@ -102,6 +111,7 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator {
|
||||
INNER JOIN ${GroupTable.TABLE_NAME} ON ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
|
||||
"""
|
||||
)
|
||||
.where("${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY} IS NOT NULL")
|
||||
.run()
|
||||
|
||||
return BackupGroupIterator(cursor)
|
||||
@@ -115,8 +125,10 @@ fun RecipientTable.restoreRecipientFromBackup(recipient: BackupRecipient, backup
|
||||
// TODO Also, should we move this when statement up to mimic the export? Kinda weird that this calls distributionListTable functions
|
||||
return when {
|
||||
recipient.contact != null -> restoreContactFromBackup(recipient.contact)
|
||||
recipient.group != null -> restoreGroupFromBackup(recipient.group)
|
||||
recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, backupState)
|
||||
recipient.self != null -> Recipient.self().id
|
||||
recipient.releaseNotes != null -> restoreReleaseNotes()
|
||||
else -> {
|
||||
Log.w(TAG, "Unrecognized recipient type!")
|
||||
null
|
||||
@@ -177,6 +189,7 @@ private fun RecipientTable.restoreContactFromBackup(contact: Contact): Recipient
|
||||
.values(
|
||||
RecipientTable.BLOCKED to contact.blocked,
|
||||
RecipientTable.HIDDEN to contact.hidden,
|
||||
RecipientTable.TYPE to RecipientTable.RecipientType.INDIVIDUAL.id,
|
||||
RecipientTable.PROFILE_FAMILY_NAME to contact.profileFamilyName.nullIfBlank(),
|
||||
RecipientTable.PROFILE_GIVEN_NAME to contact.profileGivenName.nullIfBlank(),
|
||||
RecipientTable.PROFILE_JOINED_NAME to ProfileName.fromParts(contact.profileGivenName.nullIfBlank(), contact.profileFamilyName.nullIfBlank()).toString().nullIfBlank(),
|
||||
@@ -193,6 +206,50 @@ private fun RecipientTable.restoreContactFromBackup(contact: Contact): Recipient
|
||||
return id
|
||||
}
|
||||
|
||||
private fun RecipientTable.restoreReleaseNotes(): RecipientId {
|
||||
val releaseChannelId: RecipientId = insertReleaseChannelRecipient()
|
||||
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelId)
|
||||
|
||||
setProfileName(releaseChannelId, ProfileName.asGiven("Signal"))
|
||||
setMuted(releaseChannelId, Long.MAX_VALUE)
|
||||
return releaseChannelId
|
||||
}
|
||||
|
||||
private fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
|
||||
val masterKey = GroupMasterKey(group.masterKey.toByteArray())
|
||||
val groupId = GroupId.v2(masterKey)
|
||||
|
||||
val placeholderState = DecryptedGroup.Builder()
|
||||
.revision(GroupsV2StateProcessor.PLACEHOLDER_REVISION)
|
||||
.build()
|
||||
|
||||
val values = ContentValues().apply {
|
||||
put(RecipientTable.GROUP_ID, groupId.toString())
|
||||
put(RecipientTable.AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize())
|
||||
put(RecipientTable.PROFILE_SHARING, group.whitelisted)
|
||||
put(RecipientTable.TYPE, RecipientTable.RecipientType.GV2.id)
|
||||
put(RecipientTable.STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
|
||||
if (group.hideStory) {
|
||||
val extras = RecipientExtras.Builder().hideStory(true).build()
|
||||
put(RecipientTable.EXTRAS, extras.encode())
|
||||
}
|
||||
}
|
||||
|
||||
val recipientId = writableDatabase.insert(RecipientTable.TABLE_NAME, null, values)
|
||||
val groupValues = ContentValues().apply {
|
||||
put(GroupTable.RECIPIENT_ID, recipientId)
|
||||
put(GroupTable.GROUP_ID, groupId.toString())
|
||||
put(GroupTable.TITLE, group.name)
|
||||
put(GroupTable.V2_MASTER_KEY, masterKey.serialize())
|
||||
put(GroupTable.V2_DECRYPTED_GROUP, placeholderState.encode())
|
||||
put(GroupTable.V2_REVISION, placeholderState.revision)
|
||||
put(GroupTable.SHOW_AS_STORY_STATE, group.storySendMode.toGroupShowAsStoryState().code)
|
||||
}
|
||||
writableDatabase.insert(GroupTable.TABLE_NAME, null, groupValues)
|
||||
|
||||
return RecipientId.from(recipientId)
|
||||
}
|
||||
|
||||
private fun Contact.toLocalExtras(): RecipientExtras {
|
||||
return RecipientExtras(
|
||||
hideStory = this.hideStory
|
||||
@@ -235,8 +292,8 @@ class BackupContactIterator(private val cursor: Cursor, private val selfId: Long
|
||||
return BackupRecipient(
|
||||
id = id,
|
||||
contact = Contact(
|
||||
aci = aci?.toByteArray()?.toByteString(),
|
||||
pni = pni?.toByteArray()?.toByteString(),
|
||||
aci = aci?.rawUuid?.toByteArray()?.toByteString(),
|
||||
pni = pni?.rawUuid?.toByteArray()?.toByteString(),
|
||||
username = cursor.requireString(RecipientTable.USERNAME),
|
||||
e164 = cursor.requireString(RecipientTable.E164)?.e164ToLong(),
|
||||
blocked = cursor.requireBoolean(RecipientTable.BLOCKED),
|
||||
@@ -280,7 +337,8 @@ class BackupGroupIterator(private val cursor: Cursor) : Iterator<BackupRecipient
|
||||
masterKey = cursor.requireNonNullBlob(GroupTable.V2_MASTER_KEY).toByteString(),
|
||||
whitelisted = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
|
||||
hideStory = extras?.hideStory() ?: false,
|
||||
storySendMode = showAsStoryState.toGroupStorySendMode()
|
||||
storySendMode = showAsStoryState.toGroupStorySendMode(),
|
||||
name = cursor.requireString(GroupTable.TITLE) ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -324,6 +382,14 @@ private fun GroupTable.ShowAsStoryState.toGroupStorySendMode(): Group.StorySendM
|
||||
}
|
||||
}
|
||||
|
||||
private fun Group.StorySendMode.toGroupShowAsStoryState(): GroupTable.ShowAsStoryState {
|
||||
return when (this) {
|
||||
Group.StorySendMode.ENABLED -> GroupTable.ShowAsStoryState.ALWAYS
|
||||
Group.StorySendMode.DISABLED -> GroupTable.ShowAsStoryState.NEVER
|
||||
Group.StorySendMode.DEFAULT -> GroupTable.ShowAsStoryState.IF_ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
private val Contact.formattedE164: String?
|
||||
get() {
|
||||
return e164?.let {
|
||||
|
||||
@@ -6,15 +6,16 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.database
|
||||
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toInt
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import java.io.Closeable
|
||||
@@ -22,16 +23,21 @@ import java.io.Closeable
|
||||
private val TAG = Log.tag(ThreadTable::class.java)
|
||||
|
||||
fun ThreadTable.getThreadsForBackup(): ChatIterator {
|
||||
val cursor = readableDatabase
|
||||
.select(
|
||||
ThreadTable.ID,
|
||||
ThreadTable.RECIPIENT_ID,
|
||||
ThreadTable.ARCHIVED,
|
||||
ThreadTable.PINNED,
|
||||
ThreadTable.EXPIRES_IN
|
||||
)
|
||||
.from(ThreadTable.TABLE_NAME)
|
||||
.run()
|
||||
//language=sql
|
||||
val query = """
|
||||
SELECT
|
||||
${ThreadTable.TABLE_NAME}.${ThreadTable.ID},
|
||||
${ThreadTable.RECIPIENT_ID},
|
||||
${ThreadTable.PINNED},
|
||||
${ThreadTable.READ},
|
||||
${ThreadTable.ARCHIVED},
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.MESSAGE_EXPIRATION_TIME},
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL},
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.MENTION_SETTING}
|
||||
FROM ${ThreadTable.TABLE_NAME}
|
||||
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
|
||||
"""
|
||||
val cursor = readableDatabase.query(query)
|
||||
|
||||
return ChatIterator(cursor)
|
||||
}
|
||||
@@ -43,14 +49,29 @@ fun ThreadTable.clearAllDataForBackupRestore() {
|
||||
}
|
||||
|
||||
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId): Long? {
|
||||
return writableDatabase
|
||||
val threadId = writableDatabase
|
||||
.insertInto(ThreadTable.TABLE_NAME)
|
||||
.values(
|
||||
ThreadTable.RECIPIENT_ID to recipientId.serialize(),
|
||||
ThreadTable.PINNED to chat.pinnedOrder,
|
||||
ThreadTable.ARCHIVED to chat.archived.toInt()
|
||||
ThreadTable.ARCHIVED to chat.archived.toInt(),
|
||||
ThreadTable.READ to if (chat.markedUnread) ThreadTable.ReadStatus.FORCED_UNREAD.serialize() else ThreadTable.ReadStatus.READ.serialize(),
|
||||
ThreadTable.ACTIVE to 1
|
||||
)
|
||||
.run()
|
||||
writableDatabase
|
||||
.update(
|
||||
RecipientTable.TABLE_NAME,
|
||||
contentValuesOf(
|
||||
RecipientTable.MENTION_SETTING to (if (chat.dontNotifyForMentionsIfMuted) RecipientTable.MentionSetting.DO_NOT_NOTIFY.id else RecipientTable.MentionSetting.ALWAYS_NOTIFY.id),
|
||||
RecipientTable.MUTE_UNTIL to chat.muteUntilMs,
|
||||
RecipientTable.MESSAGE_EXPIRATION_TIME to chat.expirationTimerMs
|
||||
),
|
||||
"${RecipientTable.ID} = ?",
|
||||
SqlUtil.buildArgs(recipientId.toLong())
|
||||
)
|
||||
|
||||
return threadId
|
||||
}
|
||||
|
||||
class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
|
||||
@@ -68,7 +89,10 @@ class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
|
||||
recipientId = cursor.requireLong(ThreadTable.RECIPIENT_ID),
|
||||
archived = cursor.requireBoolean(ThreadTable.ARCHIVED),
|
||||
pinnedOrder = cursor.requireInt(ThreadTable.PINNED),
|
||||
expirationTimerMs = cursor.requireLong(ThreadTable.EXPIRES_IN)
|
||||
expirationTimerMs = cursor.requireLong(RecipientTable.MESSAGE_EXPIRATION_TIME),
|
||||
muteUntilMs = cursor.requireLong(RecipientTable.MUTE_UNTIL),
|
||||
markedUnread = ThreadTable.ReadStatus.deserialize(cursor.requireInt(ThreadTable.READ)) == ThreadTable.ReadStatus.FORCED_UNREAD,
|
||||
dontNotifyForMentionsIfMuted = RecipientTable.MentionSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents
|
||||
import org.whispersystems.signalservice.api.storage.StorageRecordProtoUtil.defaultAccountRecord
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
object AccountDataProcessor {
|
||||
|
||||
@@ -47,12 +48,11 @@ object AccountDataProcessor {
|
||||
familyName = self.profileName.familyName,
|
||||
avatarUrlPath = self.profileAvatar ?: "",
|
||||
subscriptionManuallyCancelled = SignalStore.donationsValues().isUserManuallyCancelled(),
|
||||
username = SignalStore.account().username,
|
||||
username = self.username.getOrNull(),
|
||||
subscriberId = subscriber?.subscriberId?.bytes?.toByteString() ?: defaultAccountRecord.subscriberId,
|
||||
subscriberCurrencyCode = subscriber?.currencyCode ?: defaultAccountRecord.subscriberCurrencyCode,
|
||||
accountSettings = AccountData.AccountSettings(
|
||||
storyViewReceiptsEnabled = SignalStore.storyValues().viewedReceiptsEnabled,
|
||||
noteToSelfMarkedUnread = record != null && record.syncExtras.isForcedUnread,
|
||||
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context),
|
||||
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
|
||||
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
|
||||
@@ -61,13 +61,14 @@ object AccountDataProcessor {
|
||||
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
|
||||
preferContactAvatars = SignalStore.settings().isPreferSystemContactPhotos,
|
||||
universalExpireTimer = SignalStore.settings().universalExpireTimer,
|
||||
preferredReactionEmoji = SignalStore.emojiValues().reactions,
|
||||
preferredReactionEmoji = SignalStore.emojiValues().rawReactions,
|
||||
storiesDisabled = SignalStore.storyValues().isFeatureDisabled,
|
||||
hasViewedOnboardingStory = SignalStore.storyValues().userHasViewedOnboardingStory,
|
||||
hasSetMyStoriesPrivacy = SignalStore.storyValues().userHasBeenNotifiedAboutStories,
|
||||
keepMutedChatsArchived = SignalStore.settings().shouldKeepMutedChatsArchived(),
|
||||
displayBadgesOnProfile = SignalStore.donationsValues().getDisplayBadgesOnProfile(),
|
||||
hasSeenGroupStoryEducationSheet = SignalStore.storyValues().userHasSeenGroupStoryEducationSheet
|
||||
hasSeenGroupStoryEducationSheet = SignalStore.storyValues().userHasSeenGroupStoryEducationSheet,
|
||||
hasCompletedUsernameOnboarding = SignalStore.uiHints().hasCompletedUsernameOnboarding()
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -122,6 +123,14 @@ object AccountDataProcessor {
|
||||
)
|
||||
SignalStore.misc().usernameQrCodeColorScheme = accountData.usernameLink.color.toLocalUsernameColor()
|
||||
}
|
||||
|
||||
if (settings.preferredReactionEmoji.isNotEmpty()) {
|
||||
SignalStore.emojiValues().reactions = settings.preferredReactionEmoji
|
||||
}
|
||||
|
||||
if (settings.hasCompletedUsernameOnboarding) {
|
||||
SignalStore.uiHints().setHasCompletedUsernameOnboarding(true)
|
||||
}
|
||||
}
|
||||
|
||||
SignalDatabase.runPostSuccessfulTransaction { ProfileUtil.handleSelfProfileKeyChange() }
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.processor
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||
import org.thoughtcrime.securesms.backup.v2.database.getThreadsForBackup
|
||||
import org.thoughtcrime.securesms.backup.v2.database.restoreFromBackup
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
||||
@@ -18,10 +19,15 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
object ChatBackupProcessor {
|
||||
val TAG = Log.tag(ChatBackupProcessor::class.java)
|
||||
|
||||
fun export(emitter: BackupFrameEmitter) {
|
||||
fun export(exportState: ExportState, emitter: BackupFrameEmitter) {
|
||||
SignalDatabase.threads.getThreadsForBackup().use { reader ->
|
||||
for (chat in reader) {
|
||||
emitter.emit(Frame(chat = chat))
|
||||
if (exportState.recipientIds.contains(chat.recipientId)) {
|
||||
exportState.threadIds.add(chat.id)
|
||||
emitter.emit(Frame(chat = chat))
|
||||
} else {
|
||||
Log.w(TAG, "dropping thread for deleted recipient ${chat.recipientId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.processor
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
|
||||
import org.thoughtcrime.securesms.backup.v2.database.createChatItemInserter
|
||||
import org.thoughtcrime.securesms.backup.v2.database.getMessagesForBackup
|
||||
@@ -17,10 +18,12 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
object ChatItemBackupProcessor {
|
||||
val TAG = Log.tag(ChatItemBackupProcessor::class.java)
|
||||
|
||||
fun export(emitter: BackupFrameEmitter) {
|
||||
SignalDatabase.messages.getMessagesForBackup().use { chatItems ->
|
||||
fun export(exportState: ExportState, emitter: BackupFrameEmitter) {
|
||||
SignalDatabase.messages.getMessagesForBackup(exportState.backupTime).use { chatItems ->
|
||||
for (chatItem in chatItems) {
|
||||
emitter.emit(Frame(chatItem = chatItem))
|
||||
if (exportState.threadIds.contains(chatItem.chatId)) {
|
||||
emitter.emit(Frame(chatItem = chatItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,17 @@ package org.thoughtcrime.securesms.backup.v2.processor
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||
import org.thoughtcrime.securesms.backup.v2.database.BackupRecipient
|
||||
import org.thoughtcrime.securesms.backup.v2.database.getAllForBackup
|
||||
import org.thoughtcrime.securesms.backup.v2.database.getContactsForBackup
|
||||
import org.thoughtcrime.securesms.backup.v2.database.getGroupsForBackup
|
||||
import org.thoughtcrime.securesms.backup.v2.database.restoreRecipientFromBackup
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ReleaseNotes
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
typealias BackupRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
|
||||
@@ -22,12 +26,24 @@ object RecipientBackupProcessor {
|
||||
|
||||
val TAG = Log.tag(RecipientBackupProcessor::class.java)
|
||||
|
||||
fun export(emitter: BackupFrameEmitter) {
|
||||
fun export(state: ExportState, emitter: BackupFrameEmitter) {
|
||||
val selfId = Recipient.self().id.toLong()
|
||||
val releaseChannelId = SignalStore.releaseChannelValues().releaseChannelRecipientId
|
||||
if (releaseChannelId != null) {
|
||||
emitter.emit(
|
||||
Frame(
|
||||
recipient = BackupRecipient(
|
||||
id = releaseChannelId.toLong(),
|
||||
releaseNotes = ReleaseNotes()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.getContactsForBackup(selfId).use { reader ->
|
||||
for (backupRecipient in reader) {
|
||||
if (backupRecipient != null) {
|
||||
state.recipientIds.add(backupRecipient.id)
|
||||
emitter.emit(Frame(recipient = backupRecipient))
|
||||
}
|
||||
}
|
||||
@@ -35,11 +51,13 @@ object RecipientBackupProcessor {
|
||||
|
||||
SignalDatabase.recipients.getGroupsForBackup().use { reader ->
|
||||
for (backupRecipient in reader) {
|
||||
state.recipientIds.add(backupRecipient.id)
|
||||
emitter.emit(Frame(recipient = backupRecipient))
|
||||
}
|
||||
}
|
||||
|
||||
SignalDatabase.distributionLists.getAllForBackup().forEach {
|
||||
state.recipientIds.add(it.id)
|
||||
emitter.emit(Frame(recipient = it))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
|
||||
interface BackupExportWriter : AutoCloseable {
|
||||
fun write(header: BackupInfo)
|
||||
fun write(frame: Frame)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
|
||||
interface BackupImportReader : Iterator<Frame>, AutoCloseable {
|
||||
fun getHeader(): BackupInfo?
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.signal.core.util.readNBytesOrThrow
|
||||
import org.signal.core.util.readVarInt32
|
||||
import org.signal.core.util.stream.MacInputStream
|
||||
import org.signal.core.util.stream.TruncatingInputStream
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
@@ -33,8 +34,9 @@ class EncryptedBackupReader(
|
||||
aci: ACI,
|
||||
streamLength: Long,
|
||||
dataStream: () -> InputStream
|
||||
) : Iterator<Frame>, AutoCloseable {
|
||||
) : BackupImportReader {
|
||||
|
||||
val backupInfo: BackupInfo?
|
||||
var next: Frame? = null
|
||||
val stream: InputStream
|
||||
|
||||
@@ -56,10 +58,14 @@ class EncryptedBackupReader(
|
||||
cipher
|
||||
)
|
||||
)
|
||||
|
||||
backupInfo = readHeader()
|
||||
next = read()
|
||||
}
|
||||
|
||||
override fun getHeader(): BackupInfo? {
|
||||
return backupInfo
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return next != null
|
||||
}
|
||||
@@ -71,6 +77,17 @@ class EncryptedBackupReader(
|
||||
} ?: throw NoSuchElementException()
|
||||
}
|
||||
|
||||
private fun readHeader(): BackupInfo? {
|
||||
try {
|
||||
val length = stream.readVarInt32().takeIf { it >= 0 } ?: return null
|
||||
val headerBytes: ByteArray = stream.readNBytesOrThrow(length)
|
||||
|
||||
return BackupInfo.ADAPTER.decode(headerBytes)
|
||||
} catch (e: EOFException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun read(): Frame? {
|
||||
try {
|
||||
val length = stream.readVarInt32().also { if (it < 0) return null }
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import org.signal.core.util.stream.MacOutputStream
|
||||
import org.signal.core.util.writeVarInt32
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
@@ -56,6 +57,13 @@ class EncryptedBackupWriter(
|
||||
)
|
||||
}
|
||||
|
||||
override fun write(header: BackupInfo) {
|
||||
val headerBytes = header.encode()
|
||||
|
||||
mainStream.writeVarInt32(headerBytes.size)
|
||||
mainStream.write(headerBytes)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(frame: Frame) {
|
||||
val frameBytes: ByteArray = frame.encode()
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import org.signal.core.util.readNBytesOrThrow
|
||||
import org.signal.core.util.readVarInt32
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import java.io.EOFException
|
||||
import java.io.InputStream
|
||||
@@ -14,14 +15,20 @@ import java.io.InputStream
|
||||
/**
|
||||
* Reads a plaintext backup import stream one frame at a time.
|
||||
*/
|
||||
class PlainTextBackupReader(val inputStream: InputStream) : Iterator<Frame> {
|
||||
class PlainTextBackupReader(val inputStream: InputStream) : BackupImportReader {
|
||||
|
||||
val backupInfo: BackupInfo?
|
||||
var next: Frame? = null
|
||||
|
||||
init {
|
||||
backupInfo = readHeader()
|
||||
next = read()
|
||||
}
|
||||
|
||||
override fun getHeader(): BackupInfo? {
|
||||
return backupInfo
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return next != null
|
||||
}
|
||||
@@ -33,6 +40,21 @@ class PlainTextBackupReader(val inputStream: InputStream) : Iterator<Frame> {
|
||||
} ?: throw NoSuchElementException()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
inputStream.close()
|
||||
}
|
||||
|
||||
private fun readHeader(): BackupInfo? {
|
||||
try {
|
||||
val length = inputStream.readVarInt32().takeIf { it >= 0 } ?: return null
|
||||
val headerBytes: ByteArray = inputStream.readNBytesOrThrow(length)
|
||||
|
||||
return BackupInfo.ADAPTER.decode(headerBytes)
|
||||
} catch (e: EOFException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun read(): Frame? {
|
||||
try {
|
||||
val length = inputStream.readVarInt32().also { if (it < 0) return null }
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import org.signal.core.util.writeVarInt32
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
@@ -15,6 +16,14 @@ import java.io.OutputStream
|
||||
*/
|
||||
class PlainTextBackupWriter(private val outputStream: OutputStream) : BackupExportWriter {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(header: BackupInfo) {
|
||||
val headerBytes: ByteArray = header.encode()
|
||||
|
||||
outputStream.writeVarInt32(headerBytes.size)
|
||||
outputStream.write(headerBytes)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(frame: Frame) {
|
||||
val frameBytes: ByteArray = frame.encode()
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.util.getLength
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
|
||||
class MessageBackupsTestRestoreActivity : BaseActivity() {
|
||||
companion object {
|
||||
fun getIntent(context: Context): Intent {
|
||||
return Intent(context, MessageBackupsTestRestoreActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: MessageBackupsTestRestoreViewModel by viewModels()
|
||||
private lateinit var importFileLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private fun onPlaintextClicked() {
|
||||
viewModel.onPlaintextToggled()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
importFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
contentResolver.getLength(uri)?.let { length ->
|
||||
viewModel.import(length) { contentResolver.openInputStream(uri)!! }
|
||||
}
|
||||
} ?: Toast.makeText(this, "No URI selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val state by viewModel.state
|
||||
Surface {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StateLabel(text = "Plaintext?")
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Switch(
|
||||
checked = state.plaintext,
|
||||
onCheckedChange = { onPlaintextClicked() }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
type = "application/octet-stream"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
|
||||
importFileLauncher.launch(intent)
|
||||
},
|
||||
enabled = !state.importState.inProgress
|
||||
) {
|
||||
Text("Import from file")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { continueRegistration() },
|
||||
enabled = !state.importState.inProgress
|
||||
) {
|
||||
Text("Continue Reg Flow")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueRegistration() {
|
||||
if (Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(this, Recipient.self().id)) {
|
||||
val main = MainActivity.clearTop(this)
|
||||
val profile = CreateProfileActivity.getIntentForUserProfile(this)
|
||||
profile.putExtra("next_intent", main)
|
||||
startActivity(profile)
|
||||
} else {
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
ApplicationDependencies.getJobManager().add(ProfileUploadJob())
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StateLabel(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.io.InputStream
|
||||
|
||||
class MessageBackupsTestRestoreViewModel : ViewModel() {
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(importState = ImportState.NONE, plaintext = false))
|
||||
val state: State<ScreenState> = _state
|
||||
|
||||
fun import(length: Long, inputStreamFactory: () -> InputStream) {
|
||||
_state.value = _state.value.copy(importState = ImportState.IN_PROGRESS)
|
||||
|
||||
val self = Recipient.self()
|
||||
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
|
||||
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = _state.value.plaintext) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
_state.value = _state.value.copy(importState = ImportState.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPlaintextToggled() {
|
||||
_state.value = _state.value.copy(plaintext = !_state.value.plaintext)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
data class ScreenState(
|
||||
val importState: ImportState,
|
||||
val plaintext: Boolean
|
||||
)
|
||||
|
||||
enum class ImportState(val inProgress: Boolean = false) {
|
||||
NONE, IN_PROGRESS(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents a "Feature" included for a specify tier of message backups
|
||||
*/
|
||||
data class MessageBackupsTypeFeature(
|
||||
val iconResourceId: Int,
|
||||
val label: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Renders a "feature row" for a given feature.
|
||||
*/
|
||||
@Composable
|
||||
fun MessageBackupsTypeFeatureRow(
|
||||
messageBackupsTypeFeature: MessageBackupsTypeFeature,
|
||||
iconTint: Color = LocalContentColor.current,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = messageBackupsTypeFeature.iconResourceId),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsTypeFeature.label,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -19,7 +18,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -271,32 +269,8 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBackupsTypeFeatureRow(messageBackupsTypeFeature: MessageBackupsTypeFeature) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = messageBackupsTypeFeature.iconResourceId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsTypeFeature.label,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageBackupsType(
|
||||
val pricePerMonth: FiatMoney,
|
||||
val title: String,
|
||||
val features: ImmutableList<MessageBackupsTypeFeature>
|
||||
)
|
||||
|
||||
data class MessageBackupsTypeFeature(
|
||||
val iconResourceId: Int,
|
||||
val label: String
|
||||
)
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.restore
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeature
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeatureRow
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsMode
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Fragment which facilitates restoring from a backup during
|
||||
* registration.
|
||||
*/
|
||||
class RestoreFromBackupFragment : ComposeFragment() {
|
||||
|
||||
private val navArgs: RestoreFromBackupFragmentArgs by navArgs()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
RestoreFromBackupContent(
|
||||
features = persistentListOf(),
|
||||
onRestoreBackupClick = {
|
||||
// TODO [message-backups] Restore backup.
|
||||
},
|
||||
onCancelClick = {
|
||||
findNavController()
|
||||
.popBackStack()
|
||||
},
|
||||
onMoreOptionsClick = {
|
||||
findNavController()
|
||||
.safeNavigate(RestoreFromBackupFragmentDirections.actionRestoreFromBacakupFragmentToMoreOptions(MoreTransferOrRestoreOptionsMode.SELECTION))
|
||||
},
|
||||
cancelable = navArgs.cancelable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RestoreFromBackupContentPreview() {
|
||||
Previews.Preview {
|
||||
RestoreFromBackupContent(
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Your last 30 days of media"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_recent_compact_bold_16,
|
||||
label = "All of your text messages"
|
||||
)
|
||||
),
|
||||
onRestoreBackupClick = {},
|
||||
onCancelClick = {},
|
||||
onMoreOptionsClick = {},
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RestoreFromBackupContent(
|
||||
features: ImmutableList<MessageBackupsTypeFeature>,
|
||||
onRestoreBackupClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onMoreOptionsClick: () -> Unit,
|
||||
cancelable: Boolean
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.padding(top = 40.dp, bottom = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Restore from backup", // TODO [message-backups] Finalized copy.
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
val yourLastBackupText = buildAnnotatedString {
|
||||
append("Your last backup was made on March 5, 2024 at 9:00am.") // TODO [message-backups] Finalized copy.
|
||||
append(" ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) {
|
||||
append("Only media sent or received in the past 30 days is included.") // TODO [message-backups] Finalized copy.
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = yourLastBackupText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 28.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(18.dp))
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(top = 20.dp, bottom = 18.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Your backup includes:", // TODO [message-backups] Finalized copy.
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
features.forEach {
|
||||
MessageBackupsTypeFeatureRow(
|
||||
messageBackupsTypeFeature = it,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onRestoreBackupClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Restore backup" // TODO [message-backups] Finalized copy.
|
||||
)
|
||||
}
|
||||
|
||||
if (cancelable) {
|
||||
TextButton(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = android.R.string.cancel)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = onMoreOptionsClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.TransferOrRestoreFragment__more_options)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.glide.GiftBadgeModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ScreenDensity
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class BadgeImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -99,6 +100,10 @@ class BadgeImageView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun isShowingBadge(): Boolean {
|
||||
return drawable != null
|
||||
}
|
||||
|
||||
private fun clearDrawable() {
|
||||
if (drawable != null) {
|
||||
setImageDrawable(null)
|
||||
|
||||
@@ -38,7 +38,6 @@ class GiftFlowRecipientSelectionFragment : Fragment(R.layout.gift_flow_recipient
|
||||
R.id.multiselect_container,
|
||||
MultiselectForwardFragment.create(
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = false,
|
||||
multiShareArgs = emptyList(),
|
||||
forceDisableAddMessage = true,
|
||||
selectSingleRecipient = true
|
||||
|
||||
@@ -48,18 +48,25 @@ class UpdateCallLinkRepository(
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun revokeCallLink(credentials: CallLinkCredentials): Single<UpdateCallLinkResult> {
|
||||
fun deleteCallLink(credentials: CallLinkCredentials): Single<UpdateCallLinkResult> {
|
||||
return callLinkManager
|
||||
.updateCallLinkRevoked(credentials, true)
|
||||
.deleteCallLink(credentials)
|
||||
.doOnSuccess(updateState(credentials))
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun updateState(credentials: CallLinkCredentials): (UpdateCallLinkResult) -> Unit {
|
||||
return { result ->
|
||||
if (result is UpdateCallLinkResult.Success) {
|
||||
SignalDatabase.callLinks.updateCallLinkState(credentials.roomId, result.state)
|
||||
ApplicationDependencies.getJobManager().add(CallLinkUpdateSendJob(credentials.roomId))
|
||||
when (result) {
|
||||
is UpdateCallLinkResult.Update -> {
|
||||
SignalDatabase.callLinks.updateCallLinkState(credentials.roomId, result.state)
|
||||
ApplicationDependencies.getJobManager().add(CallLinkUpdateSendJob(credentials.roomId))
|
||||
}
|
||||
is UpdateCallLinkResult.Delete -> {
|
||||
SignalDatabase.callLinks.markRevoked(credentials.roomId)
|
||||
ApplicationDependencies.getJobManager().add(CallLinkUpdateSendJob(credentials.roomId))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
|
||||
private fun setCallName(callName: String) {
|
||||
lifecycleDisposable += viewModel.setCallName(callName).subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
if (it !is UpdateCallLinkResult.Update) {
|
||||
Log.w(TAG, "Failed to update call link name")
|
||||
toastFailure()
|
||||
}
|
||||
@@ -168,7 +168,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
|
||||
private fun setApproveAllMembers(approveAllMembers: Boolean) {
|
||||
lifecycleDisposable += viewModel.setApproveAllMembers(approveAllMembers).subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
if (it !is UpdateCallLinkResult.Update) {
|
||||
Log.w(TAG, "Failed to update call link restrictions")
|
||||
toastFailure()
|
||||
}
|
||||
@@ -177,7 +177,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
|
||||
private fun toggleApproveAllMembers() {
|
||||
lifecycleDisposable += viewModel.toggleApproveAllMembers().subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
if (it !is UpdateCallLinkResult.Update) {
|
||||
Log.w(TAG, "Failed to update call link restrictions")
|
||||
toastFailure()
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
@@ -119,15 +121,29 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCopyClicked() {
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.rootKeySnapshot))
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onShareLinkViaSignalClicked() {
|
||||
startActivity(
|
||||
ShareActivity.sendSimpleText(
|
||||
requireContext(),
|
||||
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDeleteClicked() {
|
||||
viewModel.setDisplayRevocationDialog(true)
|
||||
}
|
||||
|
||||
override fun onDeleteConfirmed() {
|
||||
viewModel.setDisplayRevocationDialog(false)
|
||||
lifecycleDisposable += viewModel.revoke().observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||
lifecycleDisposable += viewModel.delete().observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||
when (it) {
|
||||
is UpdateCallLinkResult.Success -> ActivityCompat.finishAfterTransition(requireActivity())
|
||||
is UpdateCallLinkResult.Update -> ActivityCompat.finishAfterTransition(requireActivity())
|
||||
else -> {
|
||||
Log.w(TAG, "Failed to revoke. $it")
|
||||
toastFailure()
|
||||
@@ -142,7 +158,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
|
||||
override fun onApproveAllMembersChanged(checked: Boolean) {
|
||||
lifecycleDisposable += viewModel.setApproveAllMembers(checked).observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
if (it !is UpdateCallLinkResult.Update) {
|
||||
Log.w(TAG, "Failed to change restrictions. $it")
|
||||
toastFailure()
|
||||
}
|
||||
@@ -151,7 +167,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
|
||||
private fun setName(name: String) {
|
||||
lifecycleDisposable += viewModel.setName(name).observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
if (it !is UpdateCallLinkResult.Update) {
|
||||
Log.w(TAG, "Failed to set name. $it")
|
||||
toastFailure()
|
||||
}
|
||||
@@ -175,6 +191,8 @@ private interface CallLinkDetailsCallback {
|
||||
fun onJoinClicked()
|
||||
fun onEditNameClicked()
|
||||
fun onShareClicked()
|
||||
fun onCopyClicked()
|
||||
fun onShareLinkViaSignalClicked()
|
||||
fun onDeleteClicked()
|
||||
fun onDeleteConfirmed()
|
||||
fun onDeleteCanceled()
|
||||
@@ -216,6 +234,8 @@ private fun CallLinkDetailsPreview() {
|
||||
override fun onJoinClicked() = Unit
|
||||
override fun onEditNameClicked() = Unit
|
||||
override fun onShareClicked() = Unit
|
||||
override fun onCopyClicked() = Unit
|
||||
override fun onShareLinkViaSignalClicked() = Unit
|
||||
override fun onDeleteClicked() = Unit
|
||||
override fun onApproveAllMembersChanged(checked: Boolean) = Unit
|
||||
}
|
||||
@@ -265,6 +285,18 @@ private fun CallLinkDetails(
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_forward_24),
|
||||
onClick = callback::onShareLinkViaSignalClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_copy_android_24),
|
||||
onClick = callback::onCopyClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24),
|
||||
|
||||
@@ -71,9 +71,9 @@ class CallLinkDetailsViewModel(
|
||||
return mutationRepository.setCallName(credentials, name)
|
||||
}
|
||||
|
||||
fun revoke(): Single<UpdateCallLinkResult> {
|
||||
fun delete(): Single<UpdateCallLinkResult> {
|
||||
val credentials = _state.value.callLink?.credentials ?: error("User cannot change the name of this call.")
|
||||
return mutationRepository.revokeCallLink(credentials)
|
||||
return mutationRepository.deleteCallLink(credentials)
|
||||
}
|
||||
|
||||
class Factory(private val callLinkRoomId: CallLinkRoomId) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -182,7 +182,7 @@ class CallLogAdapter(
|
||||
binding: CallLogAdapterItemBinding,
|
||||
private val onCallLinkClicked: (CallLogRow.CallLink) -> Unit,
|
||||
private val onCallLinkLongClicked: (View, CallLogRow.CallLink) -> Boolean,
|
||||
private val onStartVideoCallClicked: (Recipient) -> Unit
|
||||
private val onStartVideoCallClicked: (Recipient, Boolean) -> Unit
|
||||
) : BindingViewHolder<CallLinkModel, CallLogAdapterItemBinding>(binding) {
|
||||
override fun bind(model: CallLinkModel) {
|
||||
if (payload.size == 1 && payload.contains(PAYLOAD_TIMESTAMP)) {
|
||||
@@ -231,7 +231,7 @@ class CallLogAdapter(
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener {
|
||||
onStartVideoCallClicked(model.callLink.recipient)
|
||||
onStartVideoCallClicked(model.callLink.recipient, true)
|
||||
}
|
||||
binding.callType.visible = true
|
||||
binding.groupCallButton.visible = false
|
||||
@@ -243,7 +243,7 @@ class CallLogAdapter(
|
||||
private val onCallClicked: (CallLogRow.Call) -> Unit,
|
||||
private val onCallLongClicked: (View, CallLogRow.Call) -> Boolean,
|
||||
private val onStartAudioCallClicked: (Recipient) -> Unit,
|
||||
private val onStartVideoCallClicked: (Recipient) -> Unit
|
||||
private val onStartVideoCallClicked: (Recipient, Boolean) -> Unit
|
||||
) : BindingViewHolder<CallModel, CallLogAdapterItemBinding>(binding) {
|
||||
override fun bind(model: CallModel) {
|
||||
itemView.setOnClickListener {
|
||||
@@ -333,7 +333,7 @@ class CallLogAdapter(
|
||||
CallTable.Type.VIDEO_CALL -> {
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer, true) }
|
||||
binding.callType.visible = true
|
||||
binding.groupCallButton.visible = false
|
||||
}
|
||||
@@ -341,8 +341,8 @@ class CallLogAdapter(
|
||||
CallTable.Type.GROUP_CALL, CallTable.Type.AD_HOC_CALL -> {
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
binding.groupCallButton.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer, model.call.canUserBeginCall) }
|
||||
binding.groupCallButton.setOnClickListener { onStartVideoCallClicked(model.call.peer, model.call.canUserBeginCall) }
|
||||
|
||||
when (model.call.groupCallState) {
|
||||
CallLogRow.GroupCallState.NONE, CallLogRow.GroupCallState.FULL -> {
|
||||
@@ -472,6 +472,6 @@ class CallLogAdapter(
|
||||
/**
|
||||
* Invoked when user presses the video icon
|
||||
*/
|
||||
fun onStartVideoCallClicked(recipient: Recipient)
|
||||
fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -32,6 +33,7 @@ import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
@@ -45,6 +47,7 @@ import org.thoughtcrime.securesms.components.settings.app.notifications.manual.N
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationUpdateTick
|
||||
import org.thoughtcrime.securesms.conversation.SignalBottomActionBarController
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationDialogs
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationFilterBehavior
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView.OnCloseClicked
|
||||
@@ -123,6 +126,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
val callLogAdapter = CallLogAdapter(this)
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
callLogAdapter.setPagingController(viewModel.controller)
|
||||
callLogAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
(requireActivity() as? MainActivity)?.onFirstRender()
|
||||
callLogAdapter.unregisterAdapterDataObserver(this)
|
||||
}
|
||||
})
|
||||
|
||||
val scrollToPositionDelegate = ScrollToPositionDelegate(
|
||||
recyclerView = binding.recycler,
|
||||
@@ -376,8 +385,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
CommunicationActions.startVoiceCall(this, recipient)
|
||||
}
|
||||
|
||||
override fun onStartVideoCallClicked(recipient: Recipient) {
|
||||
CommunicationActions.startVideoCall(this, recipient)
|
||||
override fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean) {
|
||||
if (canUserBeginCall) {
|
||||
CommunicationActions.startVideoCall(this, recipient)
|
||||
} else {
|
||||
ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
override fun startSelection(call: CallLogRow) {
|
||||
@@ -467,7 +480,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
is CallLogDeletionResult.FailedToRevoke -> {
|
||||
errorDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(resources.getQuantityString(R.plurals.CallLogFragment__cant_delete_call_link, it.failedRevocations))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
CallLogDeletionResult.Success -> {
|
||||
|
||||
@@ -43,7 +43,9 @@ class CallLogRepository(
|
||||
|
||||
fun markAllCallEventsRead() {
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
SignalDatabase.messages.markAllCallEventsRead()
|
||||
val latestCall = SignalDatabase.calls.getLatestCall() ?: return@execute
|
||||
SignalDatabase.calls.markAllCallEventsRead()
|
||||
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forMarkedAsRead(latestCall))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,14 +96,14 @@ class CallLogRepository(
|
||||
fun deleteAllCallLogsOnOrBeforeNow(): Single<Int> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val latestTimestamp = SignalDatabase.calls.getLatestTimestamp()
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(latestTimestamp)
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(latestTimestamp)
|
||||
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forClearHistory(latestTimestamp))
|
||||
val latestCall = SignalDatabase.calls.getLatestCall() ?: return@withinTransaction
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(latestCall.timestamp)
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(latestCall.timestamp)
|
||||
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forClearHistory(latestCall))
|
||||
}
|
||||
|
||||
SignalDatabase.callLinks.getAllAdminCallLinksExcept(emptySet())
|
||||
}.flatMap(this::revokeAndCollectResults).map { 0 }.subscribeOn(Schedulers.io())
|
||||
}.flatMap(this::deleteAndCollectResults).map { 0 }.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +119,7 @@ class CallLogRepository(
|
||||
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinks(allCallLinkIds)
|
||||
SignalDatabase.callLinks.getAdminCallLinks(allCallLinkIds)
|
||||
}.flatMap(this::revokeAndCollectResults).subscribeOn(Schedulers.io())
|
||||
}.flatMap(this::deleteAndCollectResults).subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,16 +135,16 @@ class CallLogRepository(
|
||||
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
|
||||
SignalDatabase.callLinks.deleteAllNonAdminCallLinksExcept(allCallLinkIds)
|
||||
SignalDatabase.callLinks.getAllAdminCallLinksExcept(allCallLinkIds)
|
||||
}.flatMap(this::revokeAndCollectResults).subscribeOn(Schedulers.io())
|
||||
}.flatMap(this::deleteAndCollectResults).subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun revokeAndCollectResults(callLinksToRevoke: Set<CallLinkTable.CallLink>): Single<Int> {
|
||||
private fun deleteAndCollectResults(callLinksToRevoke: Set<CallLinkTable.CallLink>): Single<Int> {
|
||||
return Single.merge(
|
||||
callLinksToRevoke.map {
|
||||
updateCallLinkRepository.revokeCallLink(it.credentials!!)
|
||||
updateCallLinkRepository.deleteCallLink(it.credentials!!)
|
||||
}
|
||||
).reduce(0) { acc, current ->
|
||||
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
|
||||
acc + (if (current is UpdateCallLinkResult.Update) 0 else 1)
|
||||
}.doOnTerminate {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.doOnDispose {
|
||||
|
||||
@@ -41,6 +41,7 @@ sealed class CallLogRow {
|
||||
val children: Set<Long>,
|
||||
val searchQuery: String?,
|
||||
val callLinkPeekInfo: CallLinkPeekInfo?,
|
||||
val canUserBeginCall: Boolean,
|
||||
override val id: Id = Id.Call(children)
|
||||
) : CallLogRow()
|
||||
|
||||
|
||||
@@ -16,16 +16,14 @@ import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.InviteActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery.refresh
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientRepository
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import java.util.function.Consumer
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment.NewCallCallback {
|
||||
|
||||
@@ -46,38 +44,36 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.")
|
||||
if (SignalStore.account().isRegistered) {
|
||||
Log.i(TAG, "[onContactSelected] Doing contact refresh.")
|
||||
|
||||
val progress = SimpleProgressDialog.show(this)
|
||||
SimpleTask.run<Recipient>(lifecycle, {
|
||||
var resolved = Recipient.external(this, number!!)
|
||||
if (!resolved.isRegistered || !resolved.hasServiceId()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.")
|
||||
resolved = try {
|
||||
refresh(this, resolved, false, 10.seconds.inWholeMilliseconds)
|
||||
Recipient.resolved(resolved.id)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.")
|
||||
return@run null
|
||||
}
|
||||
}
|
||||
resolved
|
||||
}) { resolved: Recipient? ->
|
||||
|
||||
SimpleTask.run(lifecycle, { RecipientRepository.lookupNewE164(this, number!!) }, { result ->
|
||||
progress.dismiss()
|
||||
if (resolved != null) {
|
||||
if (resolved.isRegistered && resolved.hasServiceId()) {
|
||||
launch(resolved)
|
||||
} else {
|
||||
|
||||
when (result) {
|
||||
is RecipientRepository.LookupResult.Success -> {
|
||||
val resolved = Recipient.resolved(result.recipientId)
|
||||
if (resolved.isRegistered && resolved.hasServiceId()) {
|
||||
launch(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
is RecipientRepository.LookupResult.NotFound,
|
||||
is RecipientRepository.LookupResult.InvalidEntry -> {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, resolved.getDisplayName(this)))
|
||||
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, number))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
else -> {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
callback.accept(true)
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -20,8 +19,6 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class FromTextView extends SimpleEmojiTextView {
|
||||
|
||||
private static final String TAG = Log.tag(FromTextView.class);
|
||||
|
||||
public FromTextView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -31,22 +28,18 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient) {
|
||||
setText(recipient, true);
|
||||
setText(recipient, null);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read) {
|
||||
setText(recipient, read, null);
|
||||
public void setText(Recipient recipient, @Nullable CharSequence suffix) {
|
||||
setText(recipient, recipient.getDisplayName(getContext()), suffix);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||
setText(recipient, recipient.getDisplayNameOrUsername(getContext()), read, suffix);
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, @Nullable CharSequence suffix) {
|
||||
setText(recipient, fromString, suffix, true);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
|
||||
setText(recipient, fromString, read, suffix, true);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix, boolean asThread) {
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, @Nullable CharSequence suffix, boolean asThread) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
|
||||
if (asThread && recipient.isSelf()) {
|
||||
|
||||
@@ -130,9 +130,9 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
|
||||
if (previousKeyboardHeight != keyboardInsets.bottom) {
|
||||
keyboardStateListeners.forEach {
|
||||
if (previousKeyboardHeight <= 0) {
|
||||
if (previousKeyboardHeight <= 0 && keyboardInsets.bottom > 0) {
|
||||
it.onKeyboardShown()
|
||||
} else {
|
||||
} else if (previousKeyboardHeight > 0 && keyboardInsets.bottom <= 0) {
|
||||
it.onKeyboardHidden()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +351,13 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
|
||||
boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
|
||||
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
|
||||
|
||||
if (isStoryReply() && originalMissing) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentVideoOVerlayStub.setVisibility(GONE);
|
||||
attachmentNameViewStub.setVisibility(GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO [alex] -- do we need this? mainView.setMinimumHeight(isStoryReply() && originalMissing ? 0 : thumbHeight);
|
||||
thumbnailView.setPadding(0, 0, 0, 0);
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
|
||||
public class PushRegistrationReminder extends Reminder {
|
||||
|
||||
public PushRegistrationReminder(final Context context) {
|
||||
super(R.string.reminder_header_push_title, R.string.reminder_header_push_text);
|
||||
|
||||
setOkListener(v -> context.startActivity(RegistrationNavigationActivity.newIntentForReRegistration(context)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDismissable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return !SignalStore.account().isRegistered();
|
||||
}
|
||||
}
|
||||