Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20996c985b | ||
|
|
e5e9a7108e | ||
|
|
51dfee9cd3 |
@@ -1,5 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*.kt]
|
||||
indent_size = 2
|
||||
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
||||
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
custom: https://signal.org/donate/
|
||||
45
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
<!-- This is a bug report template. By following the instructions below and filling out the sections with your information, you will help the developers get all the necessary data to fix your issue.
|
||||
You can also preview your report before submitting it. You may remove sections that aren't relevant to your particular case.
|
||||
|
||||
Before we begin, please note that this tracker is only for issues. It is not for questions, comments, or feature requests.
|
||||
|
||||
If you would like to discuss a new feature or submit suggestions, please visit the community forum:
|
||||
https://community.signalusers.org
|
||||
|
||||
If you are looking for support, please visit our support center:
|
||||
https://support.signal.org/
|
||||
or email support@signal.org
|
||||
|
||||
Let's begin with a checklist: Replace the empty checkboxes [ ] below with checked ones [x] accordingly. -->
|
||||
|
||||
- [ ] I have searched open and closed issues for duplicates
|
||||
- [ ] I am submitting a bug report for existing functionality that does not work as intended
|
||||
- [ ] I have read https://github.com/signalapp/Signal-Android/wiki/Submitting-useful-bug-reports
|
||||
- [ ] This isn't a feature request or a discussion topic
|
||||
|
||||
----------------------------------------
|
||||
|
||||
### Bug description
|
||||
Describe here the issue that you are experiencing.
|
||||
|
||||
### Steps to reproduce
|
||||
- using hyphens as bullet points
|
||||
- list the steps
|
||||
- that reproduce the bug
|
||||
|
||||
**Actual result:** Describe here what happens after you run the steps above (i.e. the buggy behaviour)
|
||||
**Expected result:** Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour)
|
||||
|
||||
### Screenshots
|
||||
<!-- you can drag and drop images below -->
|
||||
|
||||
|
||||
### Device info
|
||||
<!-- replace the examples with your info -->
|
||||
**Device:** Manufacturer Model XVI
|
||||
**Android version:** 0.0.0
|
||||
**Signal version:** 0.0.0
|
||||
|
||||
### Link to debug log
|
||||
<!-- immediately after the bug has happened capture a debug log via Signal's advanced settings and paste the link below -->
|
||||
|
||||
54
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,54 +0,0 @@
|
||||
---
|
||||
name: 🛠️ Bug report
|
||||
about: Let us know that something isn't working as intended
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- This is a bug report template. By following the instructions below and filling out the sections with your information, you will help the developers get all the necessary data to fix your issue.
|
||||
You can also preview your report before submitting it. You may remove sections that aren't relevant to your particular case.
|
||||
|
||||
Before we begin, please note that this tracker is only for issues. It is not for questions, comments, or feature requests.
|
||||
|
||||
If you would like to discuss a new feature or submit suggestions, please visit the community forum:
|
||||
https://community.signalusers.org
|
||||
|
||||
If you are looking for support, please visit our support center:
|
||||
https://support.signal.org/
|
||||
or email support@signal.org
|
||||
|
||||
Let's begin with a checklist: Replace the empty checkboxes [ ] below with checked ones [x] accordingly. -->
|
||||
|
||||
- [ ] I have searched open and closed issues for duplicates
|
||||
- [ ] I am submitting a bug report for existing functionality that does not work as intended
|
||||
- [ ] I have read https://github.com/signalapp/Signal-Android/wiki/Submitting-useful-bug-reports
|
||||
- [ ] This isn't a feature request or a discussion topic
|
||||
|
||||
----------------------------------------
|
||||
|
||||
### Bug description
|
||||
Describe here the issue that you are experiencing.
|
||||
|
||||
### Steps to reproduce
|
||||
- using hyphens as bullet points
|
||||
- list the steps
|
||||
- that reproduce the bug
|
||||
|
||||
**Actual result:** Describe here what happens after you run the steps above (i.e. the buggy behaviour)
|
||||
**Expected result:** Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour)
|
||||
|
||||
### Screenshots
|
||||
<!-- you can drag and drop images below -->
|
||||
|
||||
|
||||
### Device info
|
||||
<!-- replace the examples with your info -->
|
||||
**Device:** Manufacturer Model XVI
|
||||
**Android version:** 0.0.0
|
||||
**Signal version:** 0.0.0
|
||||
|
||||
### Link to debug log
|
||||
<!-- immediately after the bug has happened capture a debug log via Signal's settings (Help -> Debug log) and paste the link below -->
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,20 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📃Support Center
|
||||
url: https://support.signal.org/
|
||||
about: Find answers to many common questions.
|
||||
- name: ✨ Feature request
|
||||
url: https://community.signalusers.org/c/feature-requests/
|
||||
about: Missing something in Signal? Let us know.
|
||||
- name: 💬 Community support
|
||||
url: https://community.signalusers.org/c/support/
|
||||
about: Feel free to ask anything.
|
||||
- name: 📖 Developer documentation
|
||||
url: https://signal.org/docs/
|
||||
about: Official Signal developer documentation.
|
||||
- name: 📚 Translation feedback.
|
||||
url: https://community.signalusers.org/c/translation-feedback/
|
||||
about: Share feedback on translations.
|
||||
- name: ❓ Other issue?
|
||||
url: https://community.signalusers.org/
|
||||
about: Search on the community forums.
|
||||
23
.github/stale.yml
vendored
@@ -1,23 +0,0 @@
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 60
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
issues:
|
||||
exemptLabels:
|
||||
- acknowledged
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: >
|
||||
This issue has been closed due to inactivity.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 1
|
||||
29
.github/workflows/android.yml
vendored
@@ -4,37 +4,28 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'master'
|
||||
- '4.**'
|
||||
- '5.**'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
cache: gradle
|
||||
java-version: 1.8
|
||||
|
||||
- name: Install NDK
|
||||
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;21.0.6113669" --sdk_root=${ANDROID_SDK_ROOT}
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew qa --parallel
|
||||
|
||||
- name: Archive reports for failed build
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: reports
|
||||
path: '*/build/reports'
|
||||
run: ./gradlew qa
|
||||
|
||||
7
.github/workflows/docker.yml
vendored
@@ -4,18 +4,15 @@ on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: cd reproducible-builds && docker build -t signal-android . && cd ..
|
||||
|
||||
- name: Test build
|
||||
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assemblePlayProdRelease
|
||||
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assembleRelease
|
||||
|
||||
1
.gitignore
vendored
@@ -27,4 +27,3 @@ obj/
|
||||
jni/libspeex/.deps/
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
maps.key
|
||||
|
||||
216
.idea/codeStyles/Project.xml
generated
@@ -1,216 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="RIGHT_MARGIN" value="240" />
|
||||
<option name="FORMATTER_TAGS_ENABLED" value="true" />
|
||||
<option name="SOFT_MARGINS" value="160" />
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
||||
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
|
||||
<option name="ALIGN_MULTILINE_ANNOTATION_PARAMETERS" value="true" />
|
||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JAVA">
|
||||
<option name="BRACE_STYLE" value="5" />
|
||||
<option name="CLASS_BRACE_STYLE" value="5" />
|
||||
<option name="METHOD_BRACE_STYLE" value="5" />
|
||||
<option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" />
|
||||
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" />
|
||||
<option name="ALIGN_MULTILINE_ASSIGNMENT" value="true" />
|
||||
<option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
|
||||
<option name="ALIGN_MULTILINE_THROWS_LIST" value="true" />
|
||||
<option name="ALIGN_MULTILINE_EXTENDS_LIST" value="true" />
|
||||
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
|
||||
<option name="ALIGN_GROUP_FIELD_DECLARATIONS" value="true" />
|
||||
<option name="ALIGN_CONSECUTIVE_VARIABLE_DECLARATIONS" value="true" />
|
||||
<option name="ALIGN_CONSECUTIVE_ASSIGNMENTS" value="true" />
|
||||
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
|
||||
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
|
||||
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
|
||||
<option name="WRAP_FIRST_METHOD_IN_CALL_CHAIN" value="true" />
|
||||
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
|
||||
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
|
||||
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
|
||||
<option name="KEEP_MULTIPLE_EXPRESSIONS_IN_ONE_LINE" value="true" />
|
||||
<option name="METHOD_ANNOTATION_WRAP" value="0" />
|
||||
<option name="CLASS_ANNOTATION_WRAP" value="0" />
|
||||
<option name="FIELD_ANNOTATION_WRAP" value="0" />
|
||||
<option name="ENUM_CONSTANTS_WRAP" value="5" />
|
||||
<option name="WRAP_ON_TYPING" value="0" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<groups>
|
||||
<group>
|
||||
<type>GETTERS_AND_SETTERS</type>
|
||||
<order>KEEP</order>
|
||||
</group>
|
||||
<group>
|
||||
<type>OVERRIDDEN_METHODS</type>
|
||||
<order>KEEP</order>
|
||||
</group>
|
||||
<group>
|
||||
<type>DEPENDENT_METHODS</type>
|
||||
<order>BREADTH_FIRST</order>
|
||||
</group>
|
||||
</groups>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
8
.idea/file.template.settings.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExportableFileTemplateSettings">
|
||||
<default_templates>
|
||||
<template name="ViewModel.kt" file-name="${NAME}ViewModel" reformat="true" live-template-enabled="false" />
|
||||
</default_templates>
|
||||
</component>
|
||||
</project>
|
||||
20
.idea/fileTemplates/ViewModel.kt
generated
@@ -1,20 +0,0 @@
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
class ${NAME}ViewModel : ViewModel() {
|
||||
|
||||
private val store = RxStore(${NAME}State())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: Flowable<${NAME}State> = store.stateFlowable
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@ Truths which we believe to be self-evident:
|
||||
1. **There is no such thing as time.** Protocol ideas that require synchronized clocks are doomed to failure.
|
||||
|
||||
|
||||
## Translations
|
||||
|
||||
Thanks to a dedicated community of volunteer translators, Signal is now available in more than one hundred languages. We use Transifex to manage our translation efforts, not GitHub. Any suggestions, corrections, or new translations should be submitted to the [Signal localization project for Android](https://www.transifex.com/signalapp/signal-android/).
|
||||
|
||||
|
||||
## Issues
|
||||
|
||||
### Useful bug reports
|
||||
@@ -70,6 +75,10 @@ There are several other ways to get involved:
|
||||
* Redirect support questions to support@signal.org and the [Signal Support Center](https://support.signal.org/).
|
||||
* Redirect non-bug discussions to the [community forum](https://community.signalusers.org).
|
||||
* Improve documentation in the [wiki](https://github.com/signalapp/Signal-Android/wiki).
|
||||
* Join the community of volunteer translators on Transifex:
|
||||
* [Android](https://www.transifex.com/signalapp/signal-android/)
|
||||
* [iOS](https://www.transifex.com/signalapp/signal-ios/)
|
||||
* [Desktop](https://www.transifex.com/signalapp/signal-desktop/)
|
||||
* Find and mark duplicate issues.
|
||||
* Try to reproduce issues and help with troubleshooting.
|
||||
* Discover solutions to open issues and post any relevant findings.
|
||||
|
||||
21
README.md
@@ -1,10 +1,10 @@
|
||||
# Signal Android
|
||||
|
||||
Signal is a simple, powerful, and secure messenger.
|
||||
Signal is a messaging app for simple private communication with friends.
|
||||
|
||||
Signal uses your phone's data connection (WiFi/3G/4G/5G) to communicate securely. Millions of people use Signal every day for free and instantaneous communication anywhere in the world. Send and receive high-fidelity messages, participate in HD voice/video calls, and explore a growing set of new features that help you stay connected. Signal’s advanced privacy-preserving technology is always enabled, so you can focus on sharing the moments that matter with the people who matter to you.
|
||||
Signal uses your phone's data connection (WiFi/3G/4G) to communicate securely, optionally supports plain SMS/MMS to function as a unified messenger, and can also encrypt the stored messages on your phone.
|
||||
|
||||
Currently available on the Play Store and [signal.org](https://signal.org/android/apk/).
|
||||
Currently available on the Play store.
|
||||
|
||||
<a href='https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height='80px'/></a>
|
||||
|
||||
@@ -18,17 +18,22 @@ Want to live life on the bleeding edge and help out with testing?
|
||||
|
||||
You can subscribe to Signal Android Beta releases here:
|
||||
https://play.google.com/apps/testing/org.thoughtcrime.securesms
|
||||
|
||||
|
||||
If you're interested in a life of peace and tranquility, stick with the standard releases.
|
||||
|
||||
## Contributing Translations
|
||||
Interested in helping to translate Signal? Contribute here:
|
||||
|
||||
https://www.transifex.com/projects/p/signal-android/
|
||||
|
||||
## Contributing Code
|
||||
|
||||
If you're new to the Signal codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar. Also please have a look at the [CONTRIBUTING.md](https://github.com/signalapp/Signal-Android/blob/main/CONTRIBUTING.md), that might answer some of your questions.
|
||||
If you're new to the Signal codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar. Also please have a look at the [CONTRIBUTING.md](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md), that might answer some of your questions.
|
||||
|
||||
For larger changes and feature ideas, we ask that you propose it on the [unofficial Community Forum](https://community.signalusers.org) for a high-level discussion with the wider community before implementation.
|
||||
|
||||
## Contributing Ideas
|
||||
Have something you want to say about Signal projects or want to be part of the conversation? Get involved in the [community forum](https://community.signalusers.org).
|
||||
Have something you want to say about Open Whisper Systems projects or want to be part of the conversation? Get involved in the [community forum](https://community.signalusers.org).
|
||||
|
||||
Help
|
||||
====
|
||||
@@ -54,8 +59,8 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2022 Signal
|
||||
Copyright 2013-2020 Signal
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
Google Play and the Google Play logo are trademarks of Google Inc.
|
||||
|
||||
@@ -96,7 +96,7 @@ try:
|
||||
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:
|
||||
except sqlite3.Error, e:
|
||||
if connection:
|
||||
connection.rollback()
|
||||
print("Error: %s" % e.args[0])
|
||||
|
||||
725
app/build.gradle
@@ -1,24 +1,54 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
import org.signal.signing.ApkSignerUtil
|
||||
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'com.google.protobuf'
|
||||
id 'androidx.navigation.safeargs'
|
||||
id 'org.jlleitschuh.gradle.ktlint'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'app.cash.exhaustive'
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.squareup.wire'
|
||||
id 'android-constants'
|
||||
id 'translations'
|
||||
import java.security.MessageDigest
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'androidx.navigation.safeargs'
|
||||
apply plugin: 'witness'
|
||||
apply from: 'translations.gradle'
|
||||
apply from: 'witness-verifications.gradle'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
||||
content {
|
||||
includeGroupByRegex "com\\.github\\.chrisbanes.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/shortcutbadger/releases/"
|
||||
content {
|
||||
includeGroupByRegex "me\\.leolin.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
|
||||
content {
|
||||
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
|
||||
content {
|
||||
includeGroupByRegex "org\\.signal.*"
|
||||
}
|
||||
}
|
||||
maven { // textdrawable
|
||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
||||
content {
|
||||
includeGroupByRegex "com\\.amulyakhare.*"
|
||||
}
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
apply from: 'static-ips.gradle'
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.18.0'
|
||||
artifact = 'com.google.protobuf:protoc:3.10.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
@@ -31,72 +61,27 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
wire {
|
||||
kotlin {
|
||||
javaInterop = true
|
||||
}
|
||||
def canonicalVersionCode = 759
|
||||
def canonicalVersionName = "5.0.9"
|
||||
|
||||
sourcePath {
|
||||
srcDir 'src/main/protowire'
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
|
||||
version = "0.47.1"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1250
|
||||
def canonicalVersionName = "6.19.2"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
'armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 5,
|
||||
'armeabi-v7a' : 6,
|
||||
'arm64-v8a' : 7,
|
||||
'x86' : 8,
|
||||
'x86_64' : 9]
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
def selectableVariants = [
|
||||
'nightlyProdSpinner',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'nightlyStagingRelease',
|
||||
'nightlyPnpPerf',
|
||||
'nightlyPnpRelease',
|
||||
'playProdDebug',
|
||||
'playProdSpinner',
|
||||
'playProdCanary',
|
||||
'playProdPerf',
|
||||
'playProdBenchmark',
|
||||
'playProdInstrumentation',
|
||||
'playProdRelease',
|
||||
'playStagingDebug',
|
||||
'playStagingCanary',
|
||||
'playStagingSpinner',
|
||||
'playStagingPerf',
|
||||
'playStagingInstrumentation',
|
||||
'playPnpDebug',
|
||||
'playPnpSpinner',
|
||||
'playStagingRelease',
|
||||
'websiteProdSpinner',
|
||||
'websiteProdRelease',
|
||||
]
|
||||
|
||||
android {
|
||||
namespace 'org.thoughtcrime.securesms'
|
||||
|
||||
buildToolsVersion = signalBuildToolsVersion
|
||||
compileSdkVersion = signalCompileSdkVersion
|
||||
buildToolsVersion BUILD_TOOL_VERSION
|
||||
compileSdkVersion COMPILE_SDK
|
||||
|
||||
flavorDimensions 'distribution', 'environment'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
testBuildType 'instrumentation'
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = ["-Xallow-result-return-type"]
|
||||
dexOptions {
|
||||
javaMaxHeapSize "4g"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -110,119 +95,40 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
|
||||
managedDevices {
|
||||
devices {
|
||||
pixel3api30 (ManagedVirtualDevice) {
|
||||
device = "Pixel 3"
|
||||
apiLevel = 30
|
||||
systemImageSource = "google-atd"
|
||||
require64Bit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
|
||||
androidTest {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility signalJavaVersion
|
||||
targetCompatibility signalJavaVersion
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['LICENSE.txt', 'LICENSE', 'NOTICE', 'asm-license.txt', 'META-INF/LICENSE', 'META-INF/LICENSE.md', 'META-INF/NOTICE', 'META-INF/LICENSE-notice.md', 'META-INF/proguard/androidx-annotations.pro', 'libsignal_jni.dylib', 'signal_jni.dll']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = '1.3.2'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion signalMinSdkVersion
|
||||
targetSdkVersion signalTargetSdkVersion
|
||||
minSdkVersion MINIMUM_SDK
|
||||
targetSdkVersion TARGET_SDK
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
project.ext.set("archivesBaseName", "Signal")
|
||||
|
||||
manifestPlaceholders = [mapsKey:"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"]
|
||||
project.ext.set("archivesBaseName", "Signal");
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\""
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}"
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\", \"https://sfu.staging.test.voip.signal.org\"}"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String[]", "SIGNAL_SERVICE_IPS", service_ips
|
||||
buildConfigField "String[]", "SIGNAL_STORAGE_IPS", storage_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDN_IPS", cdn_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDN2_IPS", cdn2_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDS_IPS", cds_ips
|
||||
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
|
||||
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
|
||||
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
|
||||
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
|
||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
|
||||
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||
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 "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
|
||||
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 "int", "TRACE_EVENT_MAX", "3500"
|
||||
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
@@ -232,15 +138,33 @@ android {
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable !project.hasProperty('generateBaselineProfile')
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
testInstrumentationRunner "org.thoughtcrime.securesms.testing.SignalTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JAVA_VERSION
|
||||
targetCompatibility JAVA_VERSION
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'LICENSE'
|
||||
exclude 'NOTICE'
|
||||
exclude 'asm-license.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -249,7 +173,7 @@ android {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
isDefault true
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||
'proguard/proguard-firebase-messaging.pro',
|
||||
'proguard/proguard-google-play-services.pro',
|
||||
@@ -258,75 +182,29 @@ android {
|
||||
'proguard/proguard-appcompat-v7.pro',
|
||||
'proguard/proguard-square-okhttp.pro',
|
||||
'proguard/proguard-square-okio.pro',
|
||||
'proguard/proguard-spongycastle.pro',
|
||||
'proguard/proguard-rounded-image-view.pro',
|
||||
'proguard/proguard-glide.pro',
|
||||
'proguard/proguard-shortcutbadger.pro',
|
||||
'proguard/proguard-retrofit.pro',
|
||||
'proguard/proguard-webrtc.pro',
|
||||
'proguard/proguard-klinker.pro',
|
||||
'proguard/proguard-mobilecoin.pro',
|
||||
'proguard/proguard-retrolambda.pro',
|
||||
'proguard/proguard-okhttp.pro',
|
||||
'proguard/proguard-ez-vcard.pro',
|
||||
'proguard/proguard.cfg'
|
||||
testProguardFiles 'proguard/proguard-automation.pro',
|
||||
'proguard/proguard.cfg'
|
||||
|
||||
manifestPlaceholders = [mapsKey:getMapsKey()]
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||
}
|
||||
|
||||
instrumentation {
|
||||
flipper {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
applicationIdSuffix ".instrumentation"
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Instrumentation\""
|
||||
}
|
||||
|
||||
spinner {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
|
||||
}
|
||||
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles = buildTypes.debug.proguardFiles
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
|
||||
}
|
||||
|
||||
perf {
|
||||
initWith debug
|
||||
isDefault false
|
||||
debuggable false
|
||||
minifyEnabled true
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
|
||||
buildConfigField "boolean", "TRACING_ENABLED", "true"
|
||||
}
|
||||
|
||||
benchmark {
|
||||
initWith debug
|
||||
isDefault false
|
||||
debuggable false
|
||||
minifyEnabled true
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Benchmark\""
|
||||
buildConfigField "boolean", "TRACING_ENABLED", "true"
|
||||
}
|
||||
|
||||
canary {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Canary\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +215,6 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"play\""
|
||||
}
|
||||
|
||||
website {
|
||||
@@ -345,25 +222,20 @@ android {
|
||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
|
||||
}
|
||||
|
||||
nightly {
|
||||
internal {
|
||||
dimension 'distribution'
|
||||
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\""
|
||||
buildConfigField "int", "TRACE_EVENT_MAX", "30_000"
|
||||
}
|
||||
|
||||
prod {
|
||||
dimension 'environment'
|
||||
|
||||
isDefault true
|
||||
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\""
|
||||
}
|
||||
|
||||
staging {
|
||||
@@ -371,261 +243,242 @@ android {
|
||||
|
||||
applicationIdSuffix ".staging"
|
||||
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
|
||||
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
|
||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
|
||||
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\""
|
||||
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 "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
||||
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
}
|
||||
|
||||
pnp {
|
||||
dimension 'environment'
|
||||
|
||||
initWith staging
|
||||
applicationIdSuffix ".pnp"
|
||||
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Pnp\""
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError true
|
||||
baseline file('lint-baseline.xml')
|
||||
checkReleaseBuilds false
|
||||
disable 'LintError'
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
if (output.baseName.contains('nightly')) {
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
|
||||
def tag = getCurrentGitTag()
|
||||
if (tag != null && tag.length() > 0) {
|
||||
if (tag.startsWith("v")) {
|
||||
tag = tag.substring(1)
|
||||
}
|
||||
output.versionNameOverride = tag
|
||||
}
|
||||
} else {
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
|
||||
android.variantFilter { variant ->
|
||||
def distribution = variant.getFlavors().get(0).name
|
||||
def environment = variant.getFlavors().get(1).name
|
||||
def buildType = variant.buildType.name
|
||||
def fullName = distribution + environment.capitalize() + buildType.capitalize()
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
disable "LintError"
|
||||
}
|
||||
|
||||
if (!selectableVariants.contains(fullName)) {
|
||||
variant.setIgnore(true)
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.androidx.fragment.ktx
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
coreLibraryDesugaring libs.android.tools.desugar
|
||||
|
||||
implementation (libs.androidx.appcompat) {
|
||||
version {
|
||||
strictly '1.6.1'
|
||||
}
|
||||
implementation ('androidx.appcompat:appcompat:1.2.0') {
|
||||
force = true
|
||||
}
|
||||
implementation libs.androidx.window.window
|
||||
implementation libs.androidx.window.java
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.material.material
|
||||
implementation libs.androidx.legacy.support
|
||||
implementation libs.androidx.preference
|
||||
implementation libs.androidx.legacy.preference
|
||||
implementation libs.androidx.gridlayout
|
||||
implementation libs.androidx.exifinterface
|
||||
implementation libs.androidx.constraintlayout
|
||||
implementation libs.androidx.multidex
|
||||
implementation libs.androidx.navigation.fragment.ktx
|
||||
implementation libs.androidx.navigation.ui.ktx
|
||||
implementation libs.androidx.lifecycle.viewmodel.ktx
|
||||
implementation libs.androidx.lifecycle.livedata.ktx
|
||||
implementation libs.androidx.lifecycle.process
|
||||
implementation libs.androidx.lifecycle.viewmodel.savedstate
|
||||
implementation libs.androidx.lifecycle.common.java8
|
||||
implementation libs.androidx.lifecycle.reactivestreams.ktx
|
||||
implementation libs.androidx.camera.core
|
||||
implementation libs.androidx.camera.camera2
|
||||
implementation libs.androidx.camera.lifecycle
|
||||
implementation libs.androidx.camera.view
|
||||
implementation libs.androidx.concurrent.futures
|
||||
implementation libs.androidx.autofill
|
||||
implementation libs.androidx.biometric
|
||||
implementation libs.androidx.sharetarget
|
||||
implementation libs.androidx.profileinstaller
|
||||
implementation libs.androidx.asynclayoutinflater
|
||||
implementation libs.androidx.asynclayoutinflater.appcompat
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
implementation "androidx.camera:camera-core:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha18"
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
implementation "androidx.autofill:autofill:1.0.0"
|
||||
implementation "androidx.paging:paging-common:2.1.2"
|
||||
implementation "androidx.paging:paging-runtime:2.1.2"
|
||||
implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
|
||||
implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1'
|
||||
|
||||
implementation (libs.firebase.messaging) {
|
||||
implementation ('com.google.firebase:firebase-messaging:20.2.0') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
|
||||
implementation libs.google.play.services.maps
|
||||
implementation libs.google.play.services.auth
|
||||
implementation 'com.google.android.gms:play-services-maps:16.1.0'
|
||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||
|
||||
implementation libs.bundles.exoplayer
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
|
||||
|
||||
implementation libs.conscrypt.android
|
||||
implementation libs.signal.aesgcmprovider
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
|
||||
implementation project(':libsignal-service')
|
||||
implementation project(':paging')
|
||||
implementation project(':core-util')
|
||||
implementation project(':glide-config')
|
||||
implementation project(':video')
|
||||
implementation project(':device-transfer')
|
||||
implementation project(':image-editor')
|
||||
implementation project(':donations')
|
||||
implementation project(':contacts')
|
||||
implementation project(':qr')
|
||||
implementation project(':sms-exporter')
|
||||
implementation project(':sticky-header-grid')
|
||||
implementation project(':photoview')
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
implementation 'org.whispersystems:signal-client-android:0.1.5'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation libs.libsignal.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
implementation 'org.signal:ringrtc-android:2.8.7'
|
||||
|
||||
implementation(libs.mobilecoin) {
|
||||
exclude group: 'com.google.protobuf'
|
||||
}
|
||||
|
||||
implementation(libs.signal.argon2) {
|
||||
artifact {
|
||||
type = "aar"
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.signal.ringrtc
|
||||
|
||||
implementation libs.leolin.shortcutbadger
|
||||
implementation libs.emilsjolander.stickylistheaders
|
||||
implementation libs.jpardogo.materialtabstrip
|
||||
implementation libs.apache.httpclient.android
|
||||
implementation libs.glide.glide
|
||||
implementation libs.roundedimageview
|
||||
implementation libs.materialish.progress
|
||||
implementation libs.greenrobot.eventbus
|
||||
implementation libs.waitingdots
|
||||
implementation libs.google.zxing.android.integration
|
||||
implementation libs.google.zxing.core
|
||||
implementation libs.google.flexbox
|
||||
implementation (libs.subsampling.scale.image.view) {
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
implementation (libs.android.tooltips) {
|
||||
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation (libs.android.smsmms) {
|
||||
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||
}
|
||||
implementation libs.stream
|
||||
implementation 'com.annimon:stream:1.1.8'
|
||||
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
||||
}
|
||||
|
||||
implementation libs.lottie
|
||||
implementation 'com.airbnb.android:lottie:3.0.7'
|
||||
|
||||
implementation libs.signal.android.database.sqlcipher
|
||||
implementation libs.androidx.sqlite
|
||||
|
||||
implementation (libs.google.ez.vcard) {
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
exclude group: 'org.freemarker'
|
||||
}
|
||||
implementation libs.dnsjava
|
||||
implementation libs.kotlinx.collections.immutable
|
||||
implementation 'dnsjava:dnsjava:2.1.9'
|
||||
|
||||
spinnerImplementation project(":spinner")
|
||||
flipperImplementation 'com.facebook.flipper:flipper:0.32.2'
|
||||
flipperImplementation 'com.facebook.soloader:soloader:0.8.2'
|
||||
|
||||
canaryImplementation libs.square.leakcanary
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:2.8.9'
|
||||
testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
|
||||
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
testImplementation testLibs.mockito.core
|
||||
testImplementation testLibs.mockito.kotlin
|
||||
|
||||
testImplementation testLibs.androidx.test.core
|
||||
testImplementation (testLibs.robolectric.robolectric) {
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.4') {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation testLibs.robolectric.shadows.multidex
|
||||
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) {
|
||||
force = true
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
|
||||
}
|
||||
|
||||
def assembleWebsiteDescriptor = { variant, file ->
|
||||
if (file.exists()) {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
file.eachByte 4096, {bytes, size ->
|
||||
md.update(bytes, 0, size);
|
||||
}
|
||||
|
||||
String digest = md.digest().collect {String.format "%02x", it}.join();
|
||||
String url = variant.productFlavors.get(0).ext.websiteUpdateUrl
|
||||
String apkName = file.getName()
|
||||
|
||||
String descriptor = "{" +
|
||||
"\"versionCode\" : ${canonicalVersionCode * postFixSize + abiPostFix['universal']}," +
|
||||
"\"versionName\" : \"$canonicalVersionName\"," +
|
||||
"\"sha256sum\" : \"$digest\"," +
|
||||
"\"url\" : \"$url/$apkName\"" +
|
||||
"}"
|
||||
|
||||
File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json"))
|
||||
|
||||
descriptorFile.write(descriptor)
|
||||
}
|
||||
testImplementation testLibs.hamcrest.hamcrest
|
||||
testImplementation testLibs.mockk
|
||||
}
|
||||
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
def signProductionRelease = { variant ->
|
||||
variant.outputs.collect { output ->
|
||||
String apkName = output.outputFile.name
|
||||
File inputFile = new File(output.outputFile.path)
|
||||
File outputFile = new File(output.outputFile.parent, apkName.replace('-unsigned', ''))
|
||||
|
||||
androidTestImplementation testLibs.androidx.test.ext.junit
|
||||
androidTestImplementation testLibs.espresso.core
|
||||
androidTestImplementation testLibs.androidx.test.core
|
||||
androidTestImplementation testLibs.androidx.test.core.ktx
|
||||
androidTestImplementation testLibs.androidx.test.ext.junit.ktx
|
||||
androidTestImplementation testLibs.mockito.android
|
||||
androidTestImplementation testLibs.mockito.kotlin
|
||||
androidTestImplementation testLibs.mockk.android
|
||||
androidTestImplementation testLibs.square.okhttp.mockserver
|
||||
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
|
||||
'pkcs11.config',
|
||||
'PKCS11',
|
||||
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
|
||||
outputFile.getAbsolutePath())
|
||||
|
||||
instrumentationImplementation (libs.androidx.fragment.testing) {
|
||||
exclude group: 'androidx.test', module: 'core'
|
||||
inputFile.delete()
|
||||
outputFile
|
||||
}
|
||||
}
|
||||
|
||||
testImplementation testLibs.espresso.core
|
||||
task signProductionPlayRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.kotlin.stdlib.jdk8
|
||||
implementation libs.kotlin.reflect
|
||||
implementation libs.jackson.module.kotlin
|
||||
task signProductionInternalRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
implementation libs.rxdogtag
|
||||
|
||||
androidTestUtil testLibs.androidx.test.orchestrator
|
||||
|
||||
implementation project(':core-ui')
|
||||
ktlintRuleset libs.ktlint.twitter.compose
|
||||
task signProductionWebsiteRelease {
|
||||
doLast {
|
||||
def variant = android.applicationVariants.find { (it.name == 'websiteProdRelease') }
|
||||
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
||||
assembleWebsiteDescriptor(variant, signedRelease)
|
||||
}
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return System.currentTimeMillis().toString()
|
||||
}
|
||||
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
exec {
|
||||
def result = exec {
|
||||
executable = 'git'
|
||||
args = ['log', '-1', '--pretty=format:%ct']
|
||||
standardOutput = os
|
||||
@@ -635,40 +488,6 @@ def getLastCommitTimestamp() {
|
||||
}
|
||||
}
|
||||
|
||||
def getGitHash() {
|
||||
if (!(new File('.git').exists())) {
|
||||
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim().substring(0, 12)
|
||||
}
|
||||
|
||||
def getCurrentGitTag() {
|
||||
if (!(new File('.git').exists())) {
|
||||
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'tag', '--points-at', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
|
||||
def output = stdout.toString().trim()
|
||||
|
||||
if (output != null && output.size() > 0) {
|
||||
def tags = output.split('\n').toList()
|
||||
return tags.stream().filter(t -> t.contains('nightly')).findFirst().orElse(tags.get(0))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
testLogging {
|
||||
events "failed"
|
||||
@@ -684,22 +503,8 @@ def loadKeystoreProperties(filename) {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
return keystoreProperties
|
||||
return keystoreProperties;
|
||||
} else {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static def getDateSuffix() {
|
||||
def date = new Date()
|
||||
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
|
||||
return formattedDate
|
||||
}
|
||||
|
||||
def getMapsKey() {
|
||||
def mapKey = file("${project.rootDir}/maps.key")
|
||||
if (mapKey.exists()) {
|
||||
return mapKey.readLines()[0]
|
||||
}
|
||||
return "AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
|
||||
}
|
||||
|
||||
@@ -25,15 +25,12 @@
|
||||
<issue id="VectorRaster" severity="error" />
|
||||
<issue id="ButtonOrder" severity="error" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="UnspecifiedImmutableFlag" severity="error" />
|
||||
|
||||
<!-- Custom lints -->
|
||||
<issue id="LogNotSignal" severity="error" />
|
||||
<issue id="LogNotAppSignal" severity="error" />
|
||||
<issue id="LogTagInlined" severity="error" />
|
||||
|
||||
<issue id="AlertDialogBuilderUsage" severity="warning" />
|
||||
|
||||
<issue id="RestrictedApi" severity="error">
|
||||
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java" />
|
||||
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java" />
|
||||
@@ -42,5 +39,4 @@
|
||||
<ignore path="*/org/thoughtcrime/securesms/jobs/StickerPackDownloadJob.java" />
|
||||
</issue>
|
||||
|
||||
<issue id="OptionalUsedAsFieldOrParameterType" severity="ignore" />
|
||||
</lint>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# MobileCoin
|
||||
-keep class com.mobilecoin.** { *; }
|
||||
@@ -2,7 +2,4 @@
|
||||
-keep class org.sqlite.database.** { *; }
|
||||
|
||||
-keep class net.sqlcipher.** { *; }
|
||||
-dontwarn net.sqlcipher.**
|
||||
|
||||
-keep class net.zetetic.** { *; }
|
||||
-dontwarn net.zetetic.**
|
||||
-dontwarn net.sqlcipher.**
|
||||
@@ -2,7 +2,6 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.whispersystems.** { *; }
|
||||
-keep class org.signal.libsignal.protocol.** { *; }
|
||||
-keep class org.thoughtcrime.securesms.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(**);
|
||||
@@ -10,5 +9,3 @@
|
||||
|
||||
# Protobuf lite
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
-keep class androidx.window.** { *; }
|
||||
@@ -1,40 +0,0 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.AndroidLogger
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
|
||||
import org.thoughtcrime.securesms.database.LogDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger
|
||||
import org.thoughtcrime.securesms.testing.InMemoryLogger
|
||||
|
||||
/**
|
||||
* Application context for running instrumentation tests (aka androidTests).
|
||||
*/
|
||||
class SignalInstrumentationApplicationContext : ApplicationContext() {
|
||||
|
||||
val inMemoryLogger: InMemoryLogger = InMemoryLogger()
|
||||
|
||||
override fun initializeAppDependencies() {
|
||||
val default = ApplicationDependencyProvider(this)
|
||||
ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
|
||||
ApplicationDependencies.getDeadlockDetector().start()
|
||||
}
|
||||
|
||||
override fun initializeLogging() {
|
||||
persistentLogger = PersistentLogger(this)
|
||||
|
||||
Log.initialize({ true }, AndroidLogger(), persistentLogger, inMemoryLogger)
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(CustomSignalProtocolLogger())
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
Log.blockUntilAllWritesFinished()
|
||||
LogDatabase.getInstance(this).trimToSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.FlakyTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.mock
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.MockProvider
|
||||
import org.thoughtcrime.securesms.testing.Post
|
||||
import org.thoughtcrime.securesms.testing.Put
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||
import org.thoughtcrime.securesms.testing.connectionFailure
|
||||
import org.thoughtcrime.securesms.testing.failure
|
||||
import org.thoughtcrime.securesms.testing.parsedRequestBody
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.thoughtcrime.securesms.testing.timeout
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ChangeNumberViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
private lateinit var kbsRepository: KbsRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().setSoTimeoutMillis(1000)
|
||||
kbsRepository = mock()
|
||||
ThreadUtil.runOnMainSync {
|
||||
viewModel = ChangeNumberViewModel(
|
||||
localNumber = harness.self.requireE164(),
|
||||
changeNumberRepository = ChangeNumberRepository(),
|
||||
savedState = SavedStateHandle(),
|
||||
password = SignalStore.account().servicePassword!!,
|
||||
verifyAccountRepository = VerifyAccountRepository(harness.application),
|
||||
kbsRepository = kbsRepository
|
||||
)
|
||||
|
||||
viewModel.setNewCountry(1)
|
||||
viewModel.setNewNationalNumber("5555550102")
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* If we encounter a server error, this means the server ack our request and rejected it. In this
|
||||
* case we know the change *did not* take on the server and can reset to a clean state.
|
||||
*/
|
||||
@Test
|
||||
fun testChangeNumber_givenServerFailedApiCall() {
|
||||
// GIVEN
|
||||
val oldPni = Recipient.self().requirePni()
|
||||
val oldE164 = Recipient.self().requireE164()
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { MockResponse().failure(500) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
||||
|
||||
// THEN
|
||||
processor.isServerSentError() assertIs true
|
||||
Recipient.self().requireE164() assertIs oldE164
|
||||
Recipient.self().requirePni() assertIs oldPni
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
|
||||
* number on the server side. We have to do a whoami call to query the server for our details and then
|
||||
* respond accordingly.
|
||||
*
|
||||
* In this case, the whoami is our old details, so we can know the change *did not* take on the server
|
||||
* and can reset to a clean state.
|
||||
*/
|
||||
@Test
|
||||
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val oldPni = Recipient.self().requirePni()
|
||||
val oldE164 = Recipient.self().requireE164()
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { MockResponse().connectionFailure() },
|
||||
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
||||
|
||||
// THEN
|
||||
processor.isServerSentError() assertIs false
|
||||
Recipient.self().requireE164() assertIs oldE164
|
||||
Recipient.self().requirePni() assertIs oldPni
|
||||
SignalStore.misc().isChangeNumberLocked assertIs false
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
|
||||
* number on the server side. We have to do a whoami call to query the server for our details and then
|
||||
* respond accordingly.
|
||||
*
|
||||
* In this case, the whoami is our new details, so we can know the change *did* take on the server
|
||||
* and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock
|
||||
* and apply the pending state after confirming the change on the server.
|
||||
*/
|
||||
@Test
|
||||
@FlakyTest
|
||||
@Ignore("Test sometimes requires manual intervention to continue.")
|
||||
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val oldPni = Recipient.self().requirePni()
|
||||
val oldE164 = Recipient.self().requireE164()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
MockResponse().timeout()
|
||||
},
|
||||
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) },
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
||||
|
||||
// THEN
|
||||
processor.isServerSentError() assertIs false
|
||||
Recipient.self().requireE164() assertIs oldE164
|
||||
Recipient.self().requirePni() assertIs oldPni
|
||||
SignalStore.misc().isChangeNumberLocked assertIs true
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNotNull()
|
||||
|
||||
// WHEN AGAIN Processing lock
|
||||
val scenario = harness.launchActivity<ChangeNumberLockActivity>()
|
||||
scenario.onActivity {}
|
||||
ThreadUtil.sleep(500)
|
||||
|
||||
// THEN AGAIN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
|
||||
MockResponse().failure(423, MockProvider.lockedFailure)
|
||||
} else {
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
}
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
|
||||
processor.registrationLock() assertIs true
|
||||
Recipient.self().requirePni() assertIsNot newPni
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
if (changeNumberRequest.deviceMessages.isEmpty()) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
}
|
||||
},
|
||||
Get("/v2/keys/$aci/2") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
|
||||
MockResponse().failure(423, MockProvider.lockedFailure)
|
||||
} else if (changeNumberRequest.deviceMessages.isEmpty()) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else if (changeNumberRequest.deviceMessages.size == 1) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2, 3)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
}
|
||||
},
|
||||
Get("/v2/keys/$aci/2") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
|
||||
},
|
||||
Get("/v2/keys/$aci/3") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3))
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
|
||||
processor.registrationLock() assertIs true
|
||||
Recipient.self().requirePni() assertIsNot newPni
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
|
||||
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore = SignalStore.account().pniPreKeys
|
||||
|
||||
Recipient.self().requireE164() assertIs "+15555550102"
|
||||
Recipient.self().requirePni() assertIs newPni
|
||||
|
||||
SignalStore.account().pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!!
|
||||
SignalStore.account().pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey
|
||||
pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId
|
||||
|
||||
val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId)
|
||||
activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey
|
||||
activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature
|
||||
|
||||
setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey
|
||||
setPreKeysRequest.preKeys assertIsSize 100
|
||||
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Helper test for rendering conversation items for preview.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Ignore("For testing/previewing manually, no assertions")
|
||||
class ConversationItemPreviewer {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(othersCount = 10)
|
||||
|
||||
@Test
|
||||
fun testShowLongName() {
|
||||
val other: Recipient = Recipient.resolved(harness.others.first())
|
||||
|
||||
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("Seef", "$$$"))
|
||||
|
||||
insertFailedMediaMessage(other = other, attachmentCount = 1)
|
||||
insertFailedMediaMessage(other = other, attachmentCount = 2)
|
||||
insertFailedMediaMessage(other = other, body = "Test", attachmentCount = 1)
|
||||
// insertFailedOutgoingMediaMessage(other = other, body = "Test", attachmentCount = 1)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
// insertMediaMessage(other = other)
|
||||
|
||||
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
|
||||
scenario.onActivity {
|
||||
}
|
||||
|
||||
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
|
||||
// ThreadUtil.sleep(45000)
|
||||
}
|
||||
|
||||
private fun insertMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
|
||||
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
|
||||
attachment()
|
||||
}
|
||||
|
||||
val message = IncomingMediaMessage(
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = System.currentTimeMillis(),
|
||||
receivedTimeMillis = System.currentTimeMillis(),
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||
)
|
||||
|
||||
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
||||
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
|
||||
private fun insertFailedMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
|
||||
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
|
||||
attachment()
|
||||
}
|
||||
|
||||
val message = IncomingMediaMessage(
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = System.currentTimeMillis(),
|
||||
receivedTimeMillis = System.currentTimeMillis(),
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||
)
|
||||
|
||||
val insert = SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
||||
|
||||
SignalDatabase.attachments.getAttachmentsForMessage(insert.messageId).forEachIndexed { index, attachment ->
|
||||
// if (index != 1) {
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, insert.messageId)
|
||||
// } else {
|
||||
// SignalDatabase.attachments.setTransferState(insert.messageId, attachment, TRANSFER_PROGRESS_STARTED)
|
||||
// }
|
||||
}
|
||||
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
|
||||
private fun insertFailedOutgoingMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
|
||||
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
|
||||
attachment()
|
||||
}
|
||||
|
||||
val message = OutgoingMessage(
|
||||
recipient = other,
|
||||
body = body,
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
isSecure = true
|
||||
)
|
||||
|
||||
val insert = SignalDatabase.messages.insertMessageOutbox(
|
||||
message,
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(other),
|
||||
false,
|
||||
null
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.getAttachmentsForMessage(insert).forEachIndexed { index, attachment ->
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, insert)
|
||||
}
|
||||
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
|
||||
private fun attachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
ReleaseChannel.CDN_NUMBER,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
"image/webp",
|
||||
null,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
1024,
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.of("/not-there.jpg"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
/**
|
||||
* Android test to help show SNC dialog quickly with custom data to make sure it displays properly.
|
||||
*/
|
||||
@Ignore("For testing/previewing manually, no assertions")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SafetyNumberChangeDialogPreviewer {
|
||||
|
||||
@get:Rule val harness = SignalActivityRule(othersCount = 10)
|
||||
|
||||
@Test
|
||||
fun testShowLongName() {
|
||||
val other: Recipient = Recipient.resolved(harness.others.first())
|
||||
|
||||
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("Super really long name like omg", "But seriously it's long like really really long"))
|
||||
|
||||
harness.setVerified(other, IdentityTable.VerifiedStatus.VERIFIED)
|
||||
harness.changeIdentityKey(other)
|
||||
|
||||
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
|
||||
scenario.onActivity {
|
||||
SafetyNumberBottomSheet.forRecipientId(other.id).show(it.supportFragmentManager)
|
||||
}
|
||||
|
||||
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
|
||||
// ThreadUtil.sleep(15000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShowLargeSheet() {
|
||||
SignalDatabase.distributionLists.setPrivacyMode(DistributionListId.MY_STORY, DistributionListPrivacyMode.ONLY_WITH)
|
||||
|
||||
val othersRecipients = harness.others.map { Recipient.resolved(it) }
|
||||
othersRecipients.forEach { other ->
|
||||
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("My", "Name"))
|
||||
|
||||
harness.setVerified(other, IdentityTable.VerifiedStatus.DEFAULT)
|
||||
harness.changeIdentityKey(other)
|
||||
|
||||
SignalDatabase.distributionLists.addMemberToList(DistributionListId.MY_STORY, DistributionListPrivacyMode.ONLY_WITH, other.id)
|
||||
}
|
||||
|
||||
val myStoryRecipientId = SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!!
|
||||
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", harness.others.first().serialize()) }
|
||||
scenario.onActivity { conversationActivity ->
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestinations(
|
||||
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
|
||||
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
|
||||
)
|
||||
.show(conversationActivity.supportFragmentManager)
|
||||
}
|
||||
|
||||
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
|
||||
// ThreadUtil.sleep( 30000)
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.FlakyTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.mms.MediaStream
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.Optional
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AttachmentTableTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABlob_whenIInsert2AttachmentsForPreUpload_thenIExpectDistinctIdsButSameFileName() {
|
||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||
val highQualityProperties = createHighQualityTransformProperties()
|
||||
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||
|
||||
assertNotEquals(attachment2.attachmentId, attachment.attachmentId)
|
||||
assertEquals(attachment2.fileName, attachment.fileName)
|
||||
}
|
||||
|
||||
@FlakyTest
|
||||
@Test
|
||||
fun givenABlobAndDifferentTransformQuality_whenIInsert2AttachmentsForPreUpload_thenIExpectDifferentFileInfos() {
|
||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||
val highQualityProperties = createHighQualityTransformProperties()
|
||||
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||
val lowQualityImage = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
|
||||
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(lowQualityImage)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||
false
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment2,
|
||||
createMediaStream(byteArrayOf(1, 2, 3)),
|
||||
false
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
|
||||
@FlakyTest
|
||||
@Test
|
||||
fun givenIdenticalAttachmentsInsertedForPreUpload_whenIUpdateAttachmentDataAndSpecifyOnlyModifyThisAttachment_thenIExpectDifferentFileInfos() {
|
||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||
val highQualityProperties = createHighQualityTransformProperties()
|
||||
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||
true
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment2,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4)),
|
||||
true
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
|
||||
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
|
||||
return UriAttachmentBuilder.build(
|
||||
id,
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
transformProperties = transformProperties
|
||||
)
|
||||
}
|
||||
|
||||
private fun createHighQualityTransformProperties(): AttachmentTable.TransformProperties {
|
||||
return AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH)
|
||||
}
|
||||
|
||||
private fun createMediaStream(byteArray: ByteArray): MediaStream {
|
||||
return MediaStream(byteArray.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
|
||||
}
|
||||
}
|
||||
@@ -1,781 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.ringrtc.CallId
|
||||
import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CallTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun givenACall_whenISetTimestamp_thenIExpectUpdatedTimestamp() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.setTimestamp(callId, conversationId, -1L)
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(-1L, call?.timestamp)
|
||||
|
||||
val messageRecord = SignalDatabase.messages.getMessageRecord(call!!.messageId!!)
|
||||
assertEquals(-1L, messageRecord.dateReceived)
|
||||
assertEquals(-1L, messageRecord.dateSent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreExistingEvent_whenIDeleteGroupCall_thenIMarkDeletedAndSetTimestamp() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
SignalDatabase.calls.deleteGroupCall(call!!)
|
||||
|
||||
val deletedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
||||
|
||||
assertEquals(CallTable.Event.DELETE, deletedCall?.event)
|
||||
assertNotEquals(0L, oldestDeletionTimestamp)
|
||||
assertNull(deletedCall!!.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
|
||||
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
||||
|
||||
assertEquals(CallTable.Event.DELETE, call?.event)
|
||||
assertNotEquals(oldestDeletionTimestamp, 0)
|
||||
assertNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenIInsertAcceptedOutgoingGroupCall_thenIExpectLocalRingerAndOutgoingRing() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
|
||||
assertEquals(harness.self.id, call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenIInsertAcceptedIncomingGroupCall_thenIExpectJoined() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.INCOMING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.JOINED, call?.event)
|
||||
assertNull(call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = harness.others[0],
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptIncomingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAMissedCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = harness.others[0],
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptIncomingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADeclinedCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = harness.others[0],
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptIncomingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGenericGroupCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptIncomingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPriorCallEventWithNewerTimestamp_whenIReceiveAGroupCallUpdateMessage_thenIExpectAnUpdatedTimestamp() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.getCallById(callId, conversationId).let {
|
||||
assertNotNull(it)
|
||||
assertEquals(now, it?.timestamp)
|
||||
}
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = 1L,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
assertEquals(1L, call?.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
||||
callId = callId,
|
||||
recipientId = harness.others[0],
|
||||
direction = CallTable.Direction.INCOMING,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = harness.others[0],
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DELETE, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGenericCallEvent_whenRingRequested_thenISetRingerAndMoveToRingingState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAJoinedCallEvent_whenRingRequested_thenISetRingerAndMoveToRingingState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGenericCallEvent_whenRingExpired_thenISetRingerAndMoveToMissedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCallEvent_whenRingExpired_thenISetRingerAndMoveToMissedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAJoinedCallEvent_whenRingIsCancelledBecauseUserIsBusyLocally_thenIMoveToAcceptedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAJoinedCallEvent_whenRingIsCancelledBecauseUserIsBusyOnAnotherDevice_thenIMoveToAcceptedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCallEvent_whenRingCancelledBecauseUserIsBusyLocally_thenIMoveToMissedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCallEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenIMoveToMissedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenACallEvent_whenRingIsAcceptedOnAnotherDevice_thenIMoveToAcceptedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAMissedCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnOutgoingRingCallEvent_whenRingDeclinedOnAnotherDevice_thenIDoNotChangeState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingRequested_thenICreateAnEventInTheRingingStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingExpired_thenICreateAnEventInTheMissedStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledByRinger_thenICreateAnEventInTheMissedStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.CANCELLED_BY_RINGER
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyLocally_thenICreateAnEventInTheMissedState() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenICreateAnEventInTheMissedState() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingAcceptedOnAnotherDevice_thenICreateAnEventInTheAcceptedState() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingDeclinedOnAnotherDevice_thenICreateAnEventInTheDeclinedState() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
/**
|
||||
* A test that guarantees that a freshly-created database looks the same as one that went through the upgrade path.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DatabaseConsistencyTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
|
||||
val testHelper = InMemoryTestHelper(ApplicationDependencies.getApplication()).also {
|
||||
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
|
||||
}
|
||||
|
||||
val upgradedStatements = testHelper.readableDatabase.getAllCreateStatements()
|
||||
|
||||
if (currentVersionStatements != upgradedStatements) {
|
||||
var message = "\n"
|
||||
|
||||
val currentByName = currentVersionStatements.associateBy { it.name }
|
||||
val upgradedByName = upgradedStatements.associateBy { it.name }
|
||||
|
||||
if (currentByName.keys != upgradedByName.keys) {
|
||||
val exclusiveToCurrent = currentByName.keys - upgradedByName.keys
|
||||
val exclusiveToUpgrade = upgradedByName.keys - currentByName.keys
|
||||
|
||||
message += "SQL entities exclusive to the newly-created database: $exclusiveToCurrent\n"
|
||||
message += "SQL entities exclusive to the upgraded database: $exclusiveToUpgrade\n\n"
|
||||
} else {
|
||||
for (currentEntry in currentByName) {
|
||||
val upgradedValue: Statement = upgradedByName[currentEntry.key]!!
|
||||
if (upgradedValue.sql != currentEntry.value.sql) {
|
||||
message += "Statement differed:\n"
|
||||
message += "newly-created:\n"
|
||||
message += "${currentEntry.value.sql}\n\n"
|
||||
message += "upgraded:\n"
|
||||
message += "${upgradedValue.sql}\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(message, false)
|
||||
}
|
||||
}
|
||||
|
||||
private data class Statement(
|
||||
val name: String,
|
||||
val sql: String
|
||||
)
|
||||
|
||||
private fun SQLiteDatabase.getAllCreateStatements(): List<Statement> {
|
||||
return this.rawQuery("SELECT name, sql FROM sqlite_schema WHERE sql NOT NULL AND name != 'sqlite_sequence'")
|
||||
.readToList { cursor ->
|
||||
Statement(
|
||||
name = cursor.requireNonNullString("name"),
|
||||
sql = cursor.requireNonNullString("sql").normalizeSql()
|
||||
)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
|
||||
private fun String.normalizeSql(): String {
|
||||
return this
|
||||
.split("\n")
|
||||
.map { it.trim() }
|
||||
.joinToString(separator = " ")
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.replace(Regex.fromLiteral("( "), "(")
|
||||
.replace(Regex.fromLiteral(" )"), ")")
|
||||
.replace(Regex("CREATE TABLE \"([a-z]+)\""), "CREATE TABLE $1") // for some reason SQLite will wrap table names in quotes for upgraded tables. This unwraps them.
|
||||
}
|
||||
|
||||
private class InMemoryTestHelper(private val application: Application) : SQLiteOpenHelper(application, null, null, 1) {
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
for (statement in SNAPSHOT_V181) {
|
||||
db.execSQL(statement.sql)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
SignalDatabaseMigrations.migrate(application, db, 181, SignalDatabaseMigrations.DATABASE_VERSION)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the list of statements that existed at version 181. Never change this.
|
||||
*/
|
||||
private val SNAPSHOT_V181 = listOf(
|
||||
Statement(
|
||||
name = "message",
|
||||
sql = "CREATE TABLE message (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n date_server INTEGER DEFAULT -1,\n thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n recipient_device_id INTEGER,\n type INTEGER NOT NULL,\n body TEXT,\n read INTEGER DEFAULT 0,\n ct_l TEXT,\n exp INTEGER,\n m_type INTEGER,\n m_size INTEGER,\n st INTEGER,\n tr_id TEXT,\n subscription_id INTEGER DEFAULT -1, \n receipt_timestamp INTEGER DEFAULT -1, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n viewed_receipt_count INTEGER DEFAULT 0,\n mismatched_identities TEXT DEFAULT NULL,\n network_failures TEXT DEFAULT NULL,\n expires_in INTEGER DEFAULT 0,\n expire_started INTEGER DEFAULT 0,\n notified INTEGER DEFAULT 0,\n quote_id INTEGER DEFAULT 0,\n quote_author INTEGER DEFAULT 0,\n quote_body TEXT DEFAULT NULL,\n quote_missing INTEGER DEFAULT 0,\n quote_mentions BLOB DEFAULT NULL,\n quote_type INTEGER DEFAULT 0,\n shared_contacts TEXT DEFAULT NULL,\n unidentified INTEGER DEFAULT 0,\n link_previews TEXT DEFAULT NULL,\n view_once INTEGER DEFAULT 0,\n reactions_unread INTEGER DEFAULT 0,\n reactions_last_seen INTEGER DEFAULT -1,\n remote_deleted INTEGER DEFAULT 0,\n mentions_self INTEGER DEFAULT 0,\n notified_timestamp INTEGER DEFAULT 0,\n server_guid TEXT DEFAULT NULL,\n message_ranges BLOB DEFAULT NULL,\n story_type INTEGER DEFAULT 0,\n parent_story_id INTEGER DEFAULT 0,\n export_state BLOB DEFAULT NULL,\n exported INTEGER DEFAULT 0,\n scheduled_date INTEGER DEFAULT -1\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "part",
|
||||
sql = "CREATE TABLE part (_id INTEGER PRIMARY KEY, mid INTEGER, seq INTEGER DEFAULT 0, ct TEXT, name TEXT, chset INTEGER, cd TEXT, fn TEXT, cid TEXT, cl TEXT, ctt_s INTEGER, ctt_t TEXT, encrypted INTEGER, pending_push INTEGER, _data TEXT, data_size INTEGER, file_name TEXT, unique_id INTEGER NOT NULL, digest BLOB, fast_preflight_id TEXT, voice_note INTEGER DEFAULT 0, borderless INTEGER DEFAULT 0, video_gif INTEGER DEFAULT 0, data_random BLOB, quote INTEGER DEFAULT 0, width INTEGER DEFAULT 0, height INTEGER DEFAULT 0, caption TEXT DEFAULT NULL, sticker_pack_id TEXT DEFAULT NULL, sticker_pack_key DEFAULT NULL, sticker_id INTEGER DEFAULT -1, sticker_emoji STRING DEFAULT NULL, data_hash TEXT DEFAULT NULL, blur_hash TEXT DEFAULT NULL, transform_properties TEXT DEFAULT NULL, transfer_file TEXT DEFAULT NULL, display_order INTEGER DEFAULT 0, upload_timestamp INTEGER DEFAULT 0, cdn_number INTEGER DEFAULT 0)"
|
||||
),
|
||||
Statement(
|
||||
name = "thread",
|
||||
sql = "CREATE TABLE thread (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n date INTEGER DEFAULT 0, \n meaningful_messages INTEGER DEFAULT 0,\n recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE,\n read INTEGER DEFAULT 1, \n type INTEGER DEFAULT 0, \n error INTEGER DEFAULT 0, \n snippet TEXT, \n snippet_type INTEGER DEFAULT 0, \n snippet_uri TEXT DEFAULT NULL, \n snippet_content_type TEXT DEFAULT NULL, \n snippet_extras TEXT DEFAULT NULL, \n unread_count INTEGER DEFAULT 0, \n archived INTEGER DEFAULT 0, \n status INTEGER DEFAULT 0, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n expires_in INTEGER DEFAULT 0, \n last_seen INTEGER DEFAULT 0, \n has_sent INTEGER DEFAULT 0, \n last_scrolled INTEGER DEFAULT 0, \n pinned INTEGER DEFAULT 0, \n unread_self_mention_count INTEGER DEFAULT 0\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "identities",
|
||||
sql = "CREATE TABLE identities (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address INTEGER UNIQUE, \n identity_key TEXT, \n first_use INTEGER DEFAULT 0, \n timestamp INTEGER DEFAULT 0, \n verified INTEGER DEFAULT 0, \n nonblocking_approval INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "drafts",
|
||||
sql = "CREATE TABLE drafts (\n _id INTEGER PRIMARY KEY, \n thread_id INTEGER, \n type TEXT, \n value TEXT\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "push",
|
||||
sql = "CREATE TABLE push (_id INTEGER PRIMARY KEY, type INTEGER, source TEXT, source_uuid TEXT, device_id INTEGER, body TEXT, content TEXT, timestamp INTEGER, server_timestamp INTEGER DEFAULT 0, server_delivered_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL)"
|
||||
),
|
||||
Statement(
|
||||
name = "groups",
|
||||
sql = "CREATE TABLE groups (\n _id INTEGER PRIMARY KEY, \n group_id TEXT, \n recipient_id INTEGER,\n title TEXT,\n avatar_id INTEGER, \n avatar_key BLOB,\n avatar_content_type TEXT, \n avatar_relay TEXT,\n timestamp INTEGER,\n active INTEGER DEFAULT 1,\n avatar_digest BLOB, \n mms INTEGER DEFAULT 0, \n master_key BLOB, \n revision BLOB, \n decrypted_group BLOB, \n expected_v2_id TEXT DEFAULT NULL, \n former_v1_members TEXT DEFAULT NULL, \n distribution_id TEXT DEFAULT NULL, \n display_as_story INTEGER DEFAULT 0, \n auth_service_id TEXT DEFAULT NULL, \n last_force_update_timestamp INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "group_membership",
|
||||
sql = "CREATE TABLE group_membership ( _id INTEGER PRIMARY KEY, group_id TEXT NOT NULL, recipient_id INTEGER NOT NULL, UNIQUE(group_id, recipient_id) )"
|
||||
),
|
||||
Statement(
|
||||
name = "recipient",
|
||||
sql = "CREATE TABLE recipient (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n uuid TEXT UNIQUE DEFAULT NULL,\n username TEXT UNIQUE DEFAULT NULL,\n phone TEXT UNIQUE DEFAULT NULL,\n email TEXT UNIQUE DEFAULT NULL,\n group_id TEXT UNIQUE DEFAULT NULL,\n group_type INTEGER DEFAULT 0,\n blocked INTEGER DEFAULT 0,\n message_ringtone TEXT DEFAULT NULL, \n message_vibrate INTEGER DEFAULT 0, \n call_ringtone TEXT DEFAULT NULL, \n call_vibrate INTEGER DEFAULT 0, \n notification_channel TEXT DEFAULT NULL, \n mute_until INTEGER DEFAULT 0, \n color TEXT DEFAULT NULL, \n seen_invite_reminder INTEGER DEFAULT 0,\n default_subscription_id INTEGER DEFAULT -1,\n message_expiration_time INTEGER DEFAULT 0,\n registered INTEGER DEFAULT 0,\n system_given_name TEXT DEFAULT NULL, \n system_family_name TEXT DEFAULT NULL, \n system_display_name TEXT DEFAULT NULL, \n system_photo_uri TEXT DEFAULT NULL, \n system_phone_label TEXT DEFAULT NULL, \n system_phone_type INTEGER DEFAULT -1, \n system_contact_uri TEXT DEFAULT NULL, \n system_info_pending INTEGER DEFAULT 0, \n profile_key TEXT DEFAULT NULL, \n profile_key_credential TEXT DEFAULT NULL, \n signal_profile_name TEXT DEFAULT NULL, \n profile_family_name TEXT DEFAULT NULL, \n profile_joined_name TEXT DEFAULT NULL, \n signal_profile_avatar TEXT DEFAULT NULL, \n profile_sharing INTEGER DEFAULT 0, \n last_profile_fetch INTEGER DEFAULT 0, \n unidentified_access_mode INTEGER DEFAULT 0, \n force_sms_selection INTEGER DEFAULT 0, \n storage_service_key TEXT UNIQUE DEFAULT NULL, \n mention_setting INTEGER DEFAULT 0, \n storage_proto TEXT DEFAULT NULL,\n capabilities INTEGER DEFAULT 0,\n last_session_reset BLOB DEFAULT NULL,\n wallpaper BLOB DEFAULT NULL,\n wallpaper_file TEXT DEFAULT NULL,\n about TEXT DEFAULT NULL,\n about_emoji TEXT DEFAULT NULL,\n extras BLOB DEFAULT NULL,\n groups_in_common INTEGER DEFAULT 0,\n chat_colors BLOB DEFAULT NULL,\n custom_chat_colors_id INTEGER DEFAULT 0,\n badges BLOB DEFAULT NULL,\n pni TEXT DEFAULT NULL,\n distribution_list_id INTEGER DEFAULT NULL,\n needs_pni_signature INTEGER DEFAULT 0,\n unregistered_timestamp INTEGER DEFAULT 0,\n hidden INTEGER DEFAULT 0,\n reporting_token BLOB DEFAULT NULL,\n system_nickname TEXT DEFAULT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_receipts",
|
||||
sql = "CREATE TABLE group_receipts (\n _id INTEGER PRIMARY KEY, \n mms_id INTEGER, \n address INTEGER, \n status INTEGER, \n timestamp INTEGER, \n unidentified INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "one_time_prekeys",
|
||||
sql = "CREATE TABLE one_time_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL, \n private_key TEXT NOT NULL,\n UNIQUE(account_id, key_id)\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "signed_prekeys",
|
||||
sql = "CREATE TABLE signed_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL,\n private_key TEXT NOT NULL,\n signature TEXT NOT NULL, \n timestamp INTEGER DEFAULT 0,\n UNIQUE(account_id, key_id)\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "sessions",
|
||||
sql = "CREATE TABLE sessions (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n account_id TEXT NOT NULL,\n address TEXT NOT NULL,\n device INTEGER NOT NULL,\n record BLOB NOT NULL,\n UNIQUE(account_id, address, device)\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "sender_keys",
|
||||
sql = "CREATE TABLE sender_keys (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n distribution_id TEXT NOT NULL,\n record BLOB NOT NULL, \n created_at INTEGER NOT NULL, \n UNIQUE(address,device, distribution_id) ON CONFLICT REPLACE\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "sender_key_shared",
|
||||
sql = "CREATE TABLE sender_key_shared (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n distribution_id TEXT NOT NULL, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n timestamp INTEGER DEFAULT 0, \n UNIQUE(distribution_id,address, device) ON CONFLICT REPLACE\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "pending_retry_receipts",
|
||||
sql = "CREATE TABLE pending_retry_receipts(_id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, device INTEGER NOT NULL, sent_timestamp INTEGER NOT NULL, received_timestamp TEXT NOT NULL, thread_id INTEGER NOT NULL, UNIQUE(author,sent_timestamp) ON CONFLICT REPLACE)"
|
||||
),
|
||||
Statement(
|
||||
name = "sticker",
|
||||
sql = "CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, pack_id TEXT NOT NULL, pack_key TEXT NOT NULL, pack_title TEXT NOT NULL, pack_author TEXT NOT NULL, sticker_id INTEGER, cover INTEGER, pack_order INTEGER, emoji TEXT NOT NULL, content_type TEXT DEFAULT NULL, last_used INTEGER, installed INTEGER,file_path TEXT NOT NULL, file_length INTEGER, file_random BLOB, UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE)"
|
||||
),
|
||||
Statement(
|
||||
name = "storage_key",
|
||||
sql = "CREATE TABLE storage_key (_id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER, key TEXT UNIQUE)"
|
||||
),
|
||||
Statement(
|
||||
name = "mention",
|
||||
sql = "CREATE TABLE mention(_id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, message_id INTEGER, recipient_id INTEGER, range_start INTEGER, range_length INTEGER)"
|
||||
),
|
||||
Statement(
|
||||
name = "payments",
|
||||
sql = "CREATE TABLE payments(_id INTEGER PRIMARY KEY, uuid TEXT DEFAULT NULL, recipient INTEGER DEFAULT 0, recipient_address TEXT DEFAULT NULL, timestamp INTEGER, note TEXT DEFAULT NULL, direction INTEGER, state INTEGER, failure_reason INTEGER, amount BLOB NOT NULL, fee BLOB NOT NULL, transaction_record BLOB DEFAULT NULL, receipt BLOB DEFAULT NULL, payment_metadata BLOB DEFAULT NULL, receipt_public_key TEXT DEFAULT NULL, block_index INTEGER DEFAULT 0, block_timestamp INTEGER DEFAULT 0, seen INTEGER, UNIQUE(uuid) ON CONFLICT ABORT)"
|
||||
),
|
||||
Statement(
|
||||
name = "chat_colors",
|
||||
sql = "CREATE TABLE chat_colors (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n chat_colors BLOB\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "emoji_search",
|
||||
sql = "CREATE TABLE emoji_search (\n _id INTEGER PRIMARY KEY,\n label TEXT NOT NULL,\n emoji TEXT NOT NULL,\n rank INTEGER DEFAULT 2147483647 \n )"
|
||||
),
|
||||
Statement(
|
||||
name = "avatar_picker",
|
||||
sql = "CREATE TABLE avatar_picker (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n last_used INTEGER DEFAULT 0,\n group_id TEXT DEFAULT NULL,\n avatar BLOB NOT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_call_ring",
|
||||
sql = "CREATE TABLE group_call_ring (\n _id INTEGER PRIMARY KEY,\n ring_id INTEGER UNIQUE,\n date_received INTEGER,\n ring_state INTEGER\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "reaction",
|
||||
sql = "CREATE TABLE reaction (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n emoji TEXT NOT NULL,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n UNIQUE(message_id, author_id) ON CONFLICT REPLACE\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "donation_receipt",
|
||||
sql = "CREATE TABLE donation_receipt (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n receipt_type TEXT NOT NULL,\n receipt_date INTEGER NOT NULL,\n amount TEXT NOT NULL,\n currency TEXT NOT NULL,\n subscription_level INTEGER NOT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "story_sends",
|
||||
sql = "CREATE TABLE story_sends (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n allows_replies INTEGER NOT NULL,\n distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "cds",
|
||||
sql = "CREATE TABLE cds (\n _id INTEGER PRIMARY KEY,\n e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE,\n last_seen_at INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "remote_megaphone",
|
||||
sql = "CREATE TABLE remote_megaphone (\n _id INTEGER PRIMARY KEY,\n uuid TEXT UNIQUE NOT NULL,\n priority INTEGER NOT NULL,\n countries TEXT,\n minimum_version INTEGER NOT NULL,\n dont_show_before INTEGER NOT NULL,\n dont_show_after INTEGER NOT NULL,\n show_for_days INTEGER NOT NULL,\n conditional_id TEXT,\n primary_action_id TEXT,\n secondary_action_id TEXT,\n image_url TEXT,\n image_uri TEXT DEFAULT NULL,\n title TEXT NOT NULL,\n body TEXT NOT NULL,\n primary_action_text TEXT,\n secondary_action_text TEXT,\n shown_at INTEGER DEFAULT 0,\n finished_at INTEGER DEFAULT 0,\n primary_action_data TEXT DEFAULT NULL,\n secondary_action_data TEXT DEFAULT NULL,\n snoozed_at INTEGER DEFAULT 0,\n seen_count INTEGER DEFAULT 0\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "pending_pni_signature_message",
|
||||
sql = "CREATE TABLE pending_pni_signature_message (\n _id INTEGER PRIMARY KEY,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n device_id INTEGER NOT NULL\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "call",
|
||||
sql = "CREATE TABLE call (\n _id INTEGER PRIMARY KEY,\n call_id INTEGER NOT NULL UNIQUE,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n peer INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n type INTEGER NOT NULL,\n direction INTEGER NOT NULL,\n event INTEGER NOT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "message_fts",
|
||||
sql = "CREATE VIRTUAL TABLE message_fts USING fts5(body, thread_id UNINDEXED, content=message, content_rowid=_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "remapped_recipients",
|
||||
sql = "CREATE TABLE remapped_recipients (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "remapped_threads",
|
||||
sql = "CREATE TABLE remapped_threads (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_payload",
|
||||
sql = "CREATE TABLE msl_payload (\n _id INTEGER PRIMARY KEY,\n date_sent INTEGER NOT NULL,\n content BLOB NOT NULL,\n content_hint INTEGER NOT NULL,\n urgent INTEGER NOT NULL DEFAULT 1\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_recipient",
|
||||
sql = "CREATE TABLE msl_recipient (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL, \n device INTEGER NOT NULL\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_message",
|
||||
sql = "CREATE TABLE msl_message (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n message_id INTEGER NOT NULL\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile",
|
||||
sql = "CREATE TABLE notification_profile (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n emoji TEXT NOT NULL,\n color TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n allow_all_calls INTEGER NOT NULL DEFAULT 0,\n allow_all_mentions INTEGER NOT NULL DEFAULT 0\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile_schedule",
|
||||
sql = "CREATE TABLE notification_profile_schedule (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n enabled INTEGER NOT NULL DEFAULT 0,\n start INTEGER NOT NULL,\n end INTEGER NOT NULL,\n days_enabled TEXT NOT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile_allowed_members",
|
||||
sql = "CREATE TABLE notification_profile_allowed_members (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL,\n UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "distribution_list",
|
||||
sql = "CREATE TABLE distribution_list (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n distribution_id TEXT UNIQUE NOT NULL,\n recipient_id INTEGER UNIQUE REFERENCES recipient (_id),\n allows_replies INTEGER DEFAULT 1,\n deletion_timestamp INTEGER DEFAULT 0,\n is_unknown INTEGER DEFAULT 0,\n privacy_mode INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "distribution_list_member",
|
||||
sql = "CREATE TABLE distribution_list_member (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id),\n privacy_mode INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "recipient_group_type_index",
|
||||
sql = "CREATE INDEX recipient_group_type_index ON recipient (group_type)"
|
||||
),
|
||||
Statement(
|
||||
name = "recipient_pni_index",
|
||||
sql = "CREATE UNIQUE INDEX recipient_pni_index ON recipient (pni)"
|
||||
),
|
||||
Statement(
|
||||
name = "recipient_service_id_profile_key",
|
||||
sql = "CREATE INDEX recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_read_and_notified_and_thread_id_index",
|
||||
sql = "CREATE INDEX mms_read_and_notified_and_thread_id_index ON message (read, notified, thread_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_type_index",
|
||||
sql = "CREATE INDEX mms_type_index ON message (type)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_date_sent_index",
|
||||
sql = "CREATE INDEX mms_date_sent_index ON message (date_sent, recipient_id, thread_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_date_server_index",
|
||||
sql = "CREATE INDEX mms_date_server_index ON message (date_server)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_thread_date_index",
|
||||
sql = "CREATE INDEX mms_thread_date_index ON message (thread_id, date_received)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_reactions_unread_index",
|
||||
sql = "CREATE INDEX mms_reactions_unread_index ON message (reactions_unread)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_story_type_index",
|
||||
sql = "CREATE INDEX mms_story_type_index ON message (story_type)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_parent_story_id_index",
|
||||
sql = "CREATE INDEX mms_parent_story_id_index ON message (parent_story_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_thread_story_parent_story_scheduled_date_index",
|
||||
sql = "CREATE INDEX mms_thread_story_parent_story_scheduled_date_index ON message (thread_id, date_received, story_type, parent_story_id, scheduled_date)"
|
||||
),
|
||||
Statement(
|
||||
name = "message_quote_id_quote_author_scheduled_date_index",
|
||||
sql = "CREATE INDEX message_quote_id_quote_author_scheduled_date_index ON message (quote_id, quote_author, scheduled_date)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_exported_index",
|
||||
sql = "CREATE INDEX mms_exported_index ON message (exported)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_id_type_payment_transactions_index",
|
||||
sql = "CREATE INDEX mms_id_type_payment_transactions_index ON message (_id,type) WHERE type & 12884901888 != 0"
|
||||
),
|
||||
Statement(
|
||||
name = "part_mms_id_index",
|
||||
sql = "CREATE INDEX part_mms_id_index ON part (mid)"
|
||||
),
|
||||
Statement(
|
||||
name = "pending_push_index",
|
||||
sql = "CREATE INDEX pending_push_index ON part (pending_push)"
|
||||
),
|
||||
Statement(
|
||||
name = "part_sticker_pack_id_index",
|
||||
sql = "CREATE INDEX part_sticker_pack_id_index ON part (sticker_pack_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "part_data_hash_index",
|
||||
sql = "CREATE INDEX part_data_hash_index ON part (data_hash)"
|
||||
),
|
||||
Statement(
|
||||
name = "part_data_index",
|
||||
sql = "CREATE INDEX part_data_index ON part (_data)"
|
||||
),
|
||||
Statement(
|
||||
name = "thread_recipient_id_index",
|
||||
sql = "CREATE INDEX thread_recipient_id_index ON thread (recipient_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "archived_count_index",
|
||||
sql = "CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)"
|
||||
),
|
||||
Statement(
|
||||
name = "thread_pinned_index",
|
||||
sql = "CREATE INDEX thread_pinned_index ON thread (pinned)"
|
||||
),
|
||||
Statement(
|
||||
name = "thread_read",
|
||||
sql = "CREATE INDEX thread_read ON thread (read)"
|
||||
),
|
||||
Statement(
|
||||
name = "draft_thread_index",
|
||||
sql = "CREATE INDEX draft_thread_index ON drafts (thread_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_id_index",
|
||||
sql = "CREATE UNIQUE INDEX group_id_index ON groups (group_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_recipient_id_index",
|
||||
sql = "CREATE UNIQUE INDEX group_recipient_id_index ON groups (recipient_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "expected_v2_id_index",
|
||||
sql = "CREATE UNIQUE INDEX expected_v2_id_index ON groups (expected_v2_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_distribution_id_index",
|
||||
sql = "CREATE UNIQUE INDEX group_distribution_id_index ON groups(distribution_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_receipt_mms_id_index",
|
||||
sql = "CREATE INDEX group_receipt_mms_id_index ON group_receipts (mms_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "sticker_pack_id_index",
|
||||
sql = "CREATE INDEX sticker_pack_id_index ON sticker (pack_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "sticker_sticker_id_index",
|
||||
sql = "CREATE INDEX sticker_sticker_id_index ON sticker (sticker_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "storage_key_type_index",
|
||||
sql = "CREATE INDEX storage_key_type_index ON storage_key (type)"
|
||||
),
|
||||
Statement(
|
||||
name = "mention_message_id_index",
|
||||
sql = "CREATE INDEX mention_message_id_index ON mention (message_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "mention_recipient_id_thread_id_index",
|
||||
sql = "CREATE INDEX mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "timestamp_direction_index",
|
||||
sql = "CREATE INDEX timestamp_direction_index ON payments (timestamp, direction)"
|
||||
),
|
||||
Statement(
|
||||
name = "timestamp_index",
|
||||
sql = "CREATE INDEX timestamp_index ON payments (timestamp)"
|
||||
),
|
||||
Statement(
|
||||
name = "receipt_public_key_index",
|
||||
sql = "CREATE UNIQUE INDEX receipt_public_key_index ON payments (receipt_public_key)"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_payload_date_sent_index",
|
||||
sql = "CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent)"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_recipient_recipient_index",
|
||||
sql = "CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_recipient_payload_index",
|
||||
sql = "CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_message_message_index",
|
||||
sql = "CREATE INDEX msl_message_message_index ON msl_message (message_id, payload_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "date_received_index",
|
||||
sql = "CREATE INDEX date_received_index on group_call_ring (date_received)"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile_schedule_profile_index",
|
||||
sql = "CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile_allowed_members_profile_index",
|
||||
sql = "CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "donation_receipt_type_index",
|
||||
sql = "CREATE INDEX donation_receipt_type_index ON donation_receipt (receipt_type)"
|
||||
),
|
||||
Statement(
|
||||
name = "donation_receipt_date_index",
|
||||
sql = "CREATE INDEX donation_receipt_date_index ON donation_receipt (receipt_date)"
|
||||
),
|
||||
Statement(
|
||||
name = "story_sends_recipient_id_sent_timestamp_allows_replies_index",
|
||||
sql = "CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)"
|
||||
),
|
||||
Statement(
|
||||
name = "story_sends_message_id_distribution_id_index",
|
||||
sql = "CREATE INDEX story_sends_message_id_distribution_id_index ON story_sends (message_id, distribution_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "distribution_list_member_list_id_recipient_id_privacy_mode_index",
|
||||
sql = "CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)"
|
||||
),
|
||||
Statement(
|
||||
name = "pending_pni_recipient_sent_device_index",
|
||||
sql = "CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON pending_pni_signature_message (recipient_id, sent_timestamp, device_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "call_call_id_index",
|
||||
sql = "CREATE INDEX call_call_id_index ON call (call_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "call_message_id_index",
|
||||
sql = "CREATE INDEX call_message_id_index ON call (message_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "message_ai",
|
||||
sql = "CREATE TRIGGER message_ai AFTER INSERT ON message BEGIN\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END"
|
||||
),
|
||||
Statement(
|
||||
name = "message_ad",
|
||||
sql = "CREATE TRIGGER message_ad AFTER DELETE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n END"
|
||||
),
|
||||
Statement(
|
||||
name = "message_au",
|
||||
sql = "CREATE TRIGGER message_au AFTER UPDATE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_message_delete",
|
||||
sql = "CREATE TRIGGER msl_message_delete AFTER DELETE ON message \n BEGIN \n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id);\n END"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_attachment_delete",
|
||||
sql = "CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part\n BEGIN\n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid);\n END"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* When writing tests, be very careful to call [DatabaseObserver.flush] before asserting any observer state. Internally, the observer is enqueueing tasks on
|
||||
* an executor, and failing to flush the executor will lead to incorrect/flaky tests.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DatabaseObserverTest {
|
||||
|
||||
private lateinit var db: SQLiteDatabase
|
||||
private lateinit var observer: DatabaseObserver
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.instance!!.signalWritableDatabase
|
||||
observer = ApplicationDependencies.getDatabaseObserver()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_runsImmediatelyIfNotInTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_runsAfterSuccessIfInTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
observer.flush()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_doesNotRunAfterFailedTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.endTransaction()
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
// Verifying we still don't run it even after a subsequent success
|
||||
db.beginTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
observer.flush()
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_onlyRunAfterAllTransactionsComplete() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.beginTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
observer.flush()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
observer.flush()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_runsImmediatelyIfTheTransactionIsOnAnotherThread() {
|
||||
db.beginTransaction()
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
observer.registerConversationObserver(1) { hasRun.set(true) }
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.flush()
|
||||
assertTrue(hasRun.get())
|
||||
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
latch.await()
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notifyConversationListeners_runsAfterSuccessIfInTransaction_ignoreDuplicateNotifications() {
|
||||
val thread1Count = AtomicInteger(0)
|
||||
val thread2Count = AtomicInteger(0)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
observer.registerConversationObserver(1) { thread1Count.incrementAndGet() }
|
||||
observer.registerConversationObserver(2) { thread2Count.incrementAndGet() }
|
||||
|
||||
observer.notifyConversationListeners(1)
|
||||
observer.notifyConversationListeners(2)
|
||||
observer.notifyConversationListeners(2)
|
||||
|
||||
observer.flush()
|
||||
assertEquals(0, thread1Count.get())
|
||||
assertEquals(0, thread2Count.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
observer.flush()
|
||||
assertEquals(1, thread1Count.get())
|
||||
assertEquals(1, thread2Count.get())
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import java.util.UUID
|
||||
|
||||
class DistributionListTablesTest {
|
||||
|
||||
private lateinit var distributionDatabase: DistributionListTables
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
distributionDatabase = SignalDatabase.distributionLists
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createList_whenNoConflict_insertSuccessfully() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createList_whenNameConflict_failToInsert() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val id2: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNull(id2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getList_returnCorrectList() {
|
||||
createRecipients(3)
|
||||
val members: List<RecipientId> = recipientList(1, 2, 3)
|
||||
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", members)
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val record: DistributionListRecord? = distributionDatabase.getList(id!!)
|
||||
Assert.assertNotNull(record)
|
||||
Assert.assertEquals(id, record!!.id)
|
||||
Assert.assertEquals("test", record.name)
|
||||
Assert.assertEquals(members, record.members)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMembers_returnsCorrectMembers() {
|
||||
createRecipients(3)
|
||||
val members: List<RecipientId> = recipientList(1, 2, 3)
|
||||
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", members)
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val foundMembers: List<RecipientId> = distributionDatabase.getMembers(id!!)
|
||||
Assert.assertEquals(members, foundMembers)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExists_getStoryType_returnsStoryWithReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val storyType = distributionDatabase.getStoryType(id!!)
|
||||
Assert.assertEquals(StoryType.STORY_WITH_REPLIES, storyType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExistsAndMarkedNoReplies_getStoryType_returnsStoryWithoutReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
distributionDatabase.setAllowsReplies(id!!, false)
|
||||
|
||||
val storyType = distributionDatabase.getStoryType(id)
|
||||
Assert.assertEquals(StoryType.STORY_WITHOUT_REPLIES, storyType)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun givenStoryDoesNotExist_getStoryType_throwsIllegalStateException() {
|
||||
distributionDatabase.getStoryType(DistributionListId.from(12))
|
||||
Assert.fail("Expected an assertion error.")
|
||||
}
|
||||
|
||||
private fun createRecipients(count: Int) {
|
||||
for (i in 0 until count) {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun recipientList(vararg ids: Long): List<RecipientId> {
|
||||
return ids.map { RecipientId.from(it) }
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import java.security.SecureRandom
|
||||
import kotlin.random.Random
|
||||
|
||||
class GroupTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var groupTable: GroupTable
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
groupTable = SignalDatabase.groups
|
||||
|
||||
groupTable.writableDatabase.delete(GroupTable.TABLE_NAME).run()
|
||||
groupTable.writableDatabase.delete(GroupTable.MembershipTable.TABLE_NAME).run()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenICreateGroupV2_thenIExpectMemberRowsPopulated() {
|
||||
val groupId = insertPushGroup()
|
||||
|
||||
//language=sql
|
||||
val members: List<RecipientId> = groupTable.writableDatabase.query(
|
||||
"""
|
||||
SELECT ${GroupTable.MembershipTable.RECIPIENT_ID}
|
||||
FROM ${GroupTable.MembershipTable.TABLE_NAME}
|
||||
WHERE ${GroupTable.MembershipTable.GROUP_ID} = "${groupId.serialize()}"
|
||||
""".trimIndent()
|
||||
).readToList {
|
||||
RecipientId.from(it.requireLong(GroupTable.RECIPIENT_ID))
|
||||
}
|
||||
|
||||
assertEquals(2, members.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupV2_whenIGetGroupsContainingMember_thenIExpectGroup() {
|
||||
val groupId = insertPushGroup()
|
||||
insertThread(groupId)
|
||||
|
||||
val groups = groupTable.getGroupsContainingMember(harness.others[0], false)
|
||||
|
||||
assertEquals(1, groups.size)
|
||||
assertEquals(groupId, groups[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnMmsGroup_whenIGetMembers_thenIExpectAllMembers() {
|
||||
val groupId = insertMmsGroup()
|
||||
|
||||
val groups = groupTable.getGroupMemberIds(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF)
|
||||
|
||||
assertEquals(2, groups.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
|
||||
insertPushGroup()
|
||||
insertMmsGroup(members = listOf(harness.others[1]))
|
||||
|
||||
val groups = groupTable.queryGroupsByMembership(
|
||||
setOf(harness.self.id, harness.others[1]),
|
||||
includeInactive = false,
|
||||
excludeV1 = false,
|
||||
excludeMms = false
|
||||
)
|
||||
|
||||
assertEquals(2, groups.cursor?.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
|
||||
insertPushGroup()
|
||||
insertMmsGroup(members = listOf(harness.others[1]))
|
||||
|
||||
val groups = groupTable.getGroups()
|
||||
|
||||
assertEquals(2, groups.cursor?.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIGetGroup_thenIExpectGroup() {
|
||||
val v2Group = insertPushGroup()
|
||||
insertThread(v2Group)
|
||||
|
||||
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||
assertEquals(setOf(harness.self.id, harness.others[0]), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndARemap_whenIGetGroup_thenIExpectRemap() {
|
||||
val v2Group = insertPushGroup()
|
||||
insertThread(v2Group)
|
||||
|
||||
groupTable.writableDatabase.withinTransaction {
|
||||
RemappedRecords.getInstance().addRecipient(harness.others[0], harness.others[1])
|
||||
}
|
||||
|
||||
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndMember_whenIIsCurrentMember_thenIExpectTrue() {
|
||||
val v2Group = insertPushGroup()
|
||||
|
||||
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
|
||||
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndMember_whenIRemove_thenIExpectNotAMember() {
|
||||
val v2Group = insertPushGroup()
|
||||
|
||||
groupTable.remove(v2Group, harness.others[0])
|
||||
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
|
||||
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndNonMember_whenIIsCurrentMember_thenIExpectFalse() {
|
||||
val v2Group = insertPushGroup()
|
||||
|
||||
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[1])
|
||||
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
|
||||
val v2Group = insertPushGroup()
|
||||
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
|
||||
val groupRecord = groupTable.getGroup(v2Group)
|
||||
|
||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
|
||||
val other = insertMmsGroup(members + listOf(harness.others[1]))
|
||||
val mmsGroup = insertMmsGroup(members)
|
||||
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
|
||||
|
||||
assertNotEquals(other, actual)
|
||||
assertEquals(mmsGroup, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleMmsGroups_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1])
|
||||
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2])
|
||||
|
||||
val group1: GroupId = insertMmsGroup(group1Members)
|
||||
val group2: GroupId = insertMmsGroup(group2Members)
|
||||
|
||||
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.toSet())
|
||||
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.toSet())
|
||||
|
||||
assertEquals(group1, group1Result)
|
||||
assertEquals(group2, group2Result)
|
||||
assertNotEquals(group1Result, group2Result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleMmsGroupsWithDifferentMemberOrders_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1], harness.others[2]).shuffled()
|
||||
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2], harness.others[3]).shuffled()
|
||||
|
||||
val group1: GroupId = insertMmsGroup(group1Members)
|
||||
val group2: GroupId = insertMmsGroup(group2Members)
|
||||
|
||||
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.shuffled().toSet())
|
||||
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.shuffled().toSet())
|
||||
|
||||
assertEquals(group1, group1Result)
|
||||
assertEquals(group2, group2Result)
|
||||
assertNotEquals(group1Result, group2Result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMmsGroupWithOneMember_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val groupMembers: List<RecipientId> = listOf(harness.self.id)
|
||||
val group: GroupId = insertMmsGroup(groupMembers)
|
||||
|
||||
val groupResult: GroupId = groupTable.getOrCreateMmsGroupForMembers(groupMembers.toSet())
|
||||
|
||||
assertEquals(group, groupResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
|
||||
val g1 = insertPushGroup(listOf())
|
||||
val g2 = insertPushGroup(listOf())
|
||||
|
||||
val gr1 = groupTable.getGroup(g1)
|
||||
val gr2 = groupTable.getGroup(g2)
|
||||
|
||||
assertEquals(g1, gr1.get().id)
|
||||
assertEquals(g2, gr2.get().id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASharedActiveGroupWithoutAThread_whenISearchForRecipientsWithGroupsInCommon_thenIExpectThatGroup() {
|
||||
val groupInCommon = insertPushGroup()
|
||||
val expected = Recipient.resolved(harness.others[0])
|
||||
|
||||
SignalDatabase.recipients.setProfileSharing(expected.id, false)
|
||||
|
||||
SignalDatabase.recipients.queryGroupMemberContacts("Buddy")!!.use {
|
||||
assertTrue(it.moveToFirst())
|
||||
assertEquals(1, it.count)
|
||||
assertEquals(expected.id.toLong(), it.requireLong(RecipientTable.ID))
|
||||
}
|
||||
|
||||
val groups = groupTable.getPushGroupsContainingMember(expected.id)
|
||||
assertEquals(1, groups.size)
|
||||
assertEquals(groups[0].id, groupInCommon)
|
||||
}
|
||||
|
||||
private fun insertThread(groupId: GroupId): Long {
|
||||
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
|
||||
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
|
||||
}
|
||||
|
||||
private fun insertMmsGroup(members: List<RecipientId> = listOf(harness.self.id, harness.others[0])): GroupId {
|
||||
val id = GroupId.createMms(SecureRandom())
|
||||
groupTable.create(
|
||||
id,
|
||||
null,
|
||||
members.apply {
|
||||
println("Creating a group with ${members.size} members")
|
||||
}
|
||||
)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
private fun insertPushGroup(
|
||||
members: List<DecryptedMember> = listOf(
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(Recipient.resolved(harness.others[0]).requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(members)
|
||||
.setRevision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageTableTest_gifts {
|
||||
private lateinit var mms: MessageTable
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
private lateinit var recipients: List<RecipientId>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mms = SignalDatabase.messages
|
||||
|
||||
mms.deleteAllThreads()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoSentGifts_whenISetOutgoingGiftsRevealed_thenIExpectEmptyList() {
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(1))
|
||||
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenSentGift_whenISetOutgoingGiftsRevealed_thenIExpectNonEmptyListContainingThatGift() {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
|
||||
assertTrue(result.isNotEmpty())
|
||||
assertEquals(messageId, result.first().messageId.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenViewedSentGift_whenISetOutgoingGiftsRevealed_thenIExpectEmptyList() {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleSentGift_whenISetOutgoingGiftsRevealedForOne_thenIExpectNonEmptyListContainingThatGift() {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(messageId, result.first().messageId.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleSentGift_whenISetOutgoingGiftsRevealedForBoth_thenIExpectNonEmptyListContainingThoseGifts() {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
|
||||
|
||||
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForBothGifts_thenIExpectNonEmptyListContainingJustThoseGifts() {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
|
||||
|
||||
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForAllThree_thenIExpectNonEmptyListContainingJustThoseGifts() {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2, messageId3))
|
||||
|
||||
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForNonGift_thenIExpectEmptyList() {
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId3))
|
||||
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Helper methods for inserting an MMS message into the MMS table.
|
||||
*/
|
||||
object MmsHelper {
|
||||
|
||||
fun insert(
|
||||
recipient: Recipient = Recipient.UNKNOWN,
|
||||
body: String = "body",
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = 0,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
|
||||
threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient, distributionType),
|
||||
storyType: StoryType = StoryType.NONE,
|
||||
parentStoryId: ParentStoryId? = null,
|
||||
isStoryReaction: Boolean = false,
|
||||
giftBadge: GiftBadge? = null,
|
||||
secure: Boolean = true
|
||||
): Long {
|
||||
val message = OutgoingMessage(
|
||||
recipient = recipient,
|
||||
body = body,
|
||||
timestamp = sentTimeMillis,
|
||||
subscriptionId = subscriptionId,
|
||||
expiresIn = expiresIn,
|
||||
viewOnce = viewOnce,
|
||||
distributionType = distributionType,
|
||||
storyType = storyType,
|
||||
parentStoryId = parentStoryId,
|
||||
isStoryReaction = isStoryReaction,
|
||||
giftBadge = giftBadge,
|
||||
isSecure = secure
|
||||
)
|
||||
|
||||
return insert(
|
||||
message = message,
|
||||
threadId = threadId
|
||||
)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: OutgoingMessage,
|
||||
threadId: Long
|
||||
): Long {
|
||||
return SignalDatabase.messages.insertMessageOutbox(message, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: IncomingMediaMessage,
|
||||
threadId: Long
|
||||
): Optional<MessageTable.InsertResult> {
|
||||
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, threadId)
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MmsTableTest_stories {
|
||||
|
||||
private lateinit var mms: MessageTable
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
private lateinit var myStory: Recipient
|
||||
private lateinit var recipients: List<RecipientId>
|
||||
private lateinit var releaseChannelRecipient: Recipient
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mms = SignalDatabase.messages
|
||||
|
||||
mms.deleteAllThreads()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
|
||||
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
|
||||
|
||||
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectAnEmptyList() {
|
||||
// WHEN
|
||||
val result = mms.getOrderedStoryRecipientsAndIds(false)
|
||||
|
||||
// THEN
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenOneOutgoingAndOneIncomingStory_whenIGetOrderedStoryRecipientsAndIds_thenIExpectIncomingThenOutgoing() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
|
||||
val sender = recipients[0]
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 1,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = threadId
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = sender,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
-1L
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.getOrderedStoryRecipientsAndIds(false)
|
||||
|
||||
// THEN
|
||||
assertEquals(listOf(sender.toLong(), myStory.id.toLong()), result.map { it.recipientId.toLong() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAStory_whenISetIncomingStoryMessageViewed_thenIExpectASetReceiptTimestamp() {
|
||||
// GIVEN
|
||||
val sender = recipients[0]
|
||||
val messageId = MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = sender,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
-1L
|
||||
).get().messageId
|
||||
|
||||
val messageBeforeMark = SignalDatabase.messages.getMessageRecord(messageId)
|
||||
assertFalse(messageBeforeMark.incomingStoryViewedAtTimestamp > 0)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.messages.setIncomingMessageViewed(messageId)
|
||||
|
||||
// THEN
|
||||
val messageAfterMark = SignalDatabase.messages.getMessageRecord(messageId)
|
||||
assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0)
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
fun given5ViewedStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectLatestViewedFirst() {
|
||||
// GIVEN
|
||||
val messageIds = recipients.take(5).map {
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = it,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
-1L
|
||||
).get().messageId
|
||||
}
|
||||
|
||||
val randomizedOrderedIds = messageIds.shuffled()
|
||||
randomizedOrderedIds.forEach {
|
||||
SignalDatabase.messages.setIncomingMessageViewed(it)
|
||||
Thread.sleep(5)
|
||||
}
|
||||
|
||||
// WHEN
|
||||
val result = SignalDatabase.messages.getOrderedStoryRecipientsAndIds(false)
|
||||
val resultOrderedIds = result.map { it.messageId }
|
||||
|
||||
// THEN
|
||||
assertEquals(randomizedOrderedIds.reversed(), resultOrderedIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun given15Stories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectUnviewedThenInterspersedViewedAndSelfSendsAllDescending() {
|
||||
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
|
||||
|
||||
val unviewedIds: List<Long> = (0 until 5).map {
|
||||
Thread.sleep(5)
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = recipients[it],
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
-1L
|
||||
).get().messageId
|
||||
}
|
||||
|
||||
val viewedIds: List<Long> = (0 until 5).map {
|
||||
Thread.sleep(5)
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = recipients[it],
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
-1L
|
||||
).get().messageId
|
||||
}
|
||||
|
||||
val interspersedIds: List<Long> = (0 until 10).map {
|
||||
Thread.sleep(5)
|
||||
if (it % 2 == 0) {
|
||||
SignalDatabase.messages.setIncomingMessageViewed(viewedIds[it / 2])
|
||||
viewedIds[it / 2]
|
||||
} else {
|
||||
MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = myStoryThread
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val result = SignalDatabase.messages.getOrderedStoryRecipientsAndIds(false)
|
||||
val resultOrderedIds = result.map { it.messageId }
|
||||
|
||||
assertEquals(unviewedIds.reversed() + interspersedIds.reversed(), resultOrderedIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
|
||||
// WHEN
|
||||
val result = mms.isOutgoingStoryAlreadyInDatabase(recipients[0], 200)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoOutgoingStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = recipients[0],
|
||||
sentTimeMillis = 200,
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
-1L
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.isOutgoingStoryAlreadyInDatabase(recipients[0], 200)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenOutgoingStoryExistsForRecipientAndTime_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.isOutgoingStoryAlreadyInDatabase(myStory.id, 200)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupStoryWithNoReplies_whenICheckHasSelfReplyInGroupStory_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
fun givenAGroupStoryWithAReplyFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 201,
|
||||
storyType = StoryType.NONE,
|
||||
parentStoryId = ParentStoryId.GroupReply(groupStoryId)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupStoryWithAReactionFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 201,
|
||||
storyType = StoryType.NONE,
|
||||
parentStoryId = ParentStoryId.GroupReply(groupStoryId),
|
||||
isStoryReaction = true
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupStoryWithAReplyFromSomeoneElse_whenICheckHasSelfReplyInGroupStory_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = myStory.id,
|
||||
sentTimeMillis = 201,
|
||||
serverTimeMillis = 201,
|
||||
receivedTimeMillis = 202,
|
||||
parentStoryId = ParentStoryId.GroupReply(groupStoryId)
|
||||
),
|
||||
-1
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNotViewedOnboardingAndOnlyStoryIsOnboardingAndAdded2DaysAgo_whenIGetOldestStoryTimestamp_thenIExpectNull() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(releaseChannelRecipient)
|
||||
MmsHelper.insert(
|
||||
recipient = releaseChannelRecipient,
|
||||
sentTimeMillis = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2),
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = threadId
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val oldestTimestamp = SignalDatabase.messages.getOldestStorySendTimestamp(false)
|
||||
|
||||
// THEN
|
||||
assertNull(oldestTimestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenViewedOnboardingAndOnlyStoryIsOnboardingAndAdded2DaysAgo_whenIGetOldestStoryTimestamp_thenIExpectNotNull() {
|
||||
// GIVEN
|
||||
val expected = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(releaseChannelRecipient)
|
||||
MmsHelper.insert(
|
||||
recipient = releaseChannelRecipient,
|
||||
sentTimeMillis = expected,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = threadId
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val oldestTimestamp = SignalDatabase.messages.getOldestStorySendTimestamp(true)
|
||||
|
||||
// THEN
|
||||
assertEquals(expected, oldestTimestamp)
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun givenAHiddenRecipient_whenIQueryAllContacts_thenIDoNotExpectHiddenToBeReturned() {
|
||||
val hiddenRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results = SignalDatabase.recipients.queryAllContacts("Hidden")!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAHiddenRecipient_whenIGetSignalContacts_thenIDoNotExpectHiddenToBeReturned() {
|
||||
val hiddenRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(false)?.use {
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
}!!
|
||||
|
||||
assertNotEquals(0, results.size)
|
||||
assertFalse(hiddenRecipient in results)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAHiddenRecipient_whenIQuerySignalContacts_thenIDoNotExpectHiddenToBeReturned() {
|
||||
val hiddenRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAHiddenRecipient_whenIQueryNonGroupContacts_thenIDoNotExpectHiddenToBeReturned() {
|
||||
val hiddenRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results = SignalDatabase.recipients.queryNonGroupContacts("Hidden", false)!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAHiddenRecipient_whenIGetNonGroupContacts_thenIDoNotExpectHiddenToBeReturned() {
|
||||
val hiddenRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(false)?.use {
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
}!!
|
||||
|
||||
assertNotEquals(0, results.size)
|
||||
assertFalse(hiddenRecipient in results)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABlockedRecipient_whenIQueryAllContacts_thenIDoNotExpectBlockedToBeReturned() {
|
||||
val blockedRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
||||
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
||||
|
||||
val results = SignalDatabase.recipients.queryAllContacts("Blocked")!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABlockedRecipient_whenIGetSignalContacts_thenIDoNotExpectBlockedToBeReturned() {
|
||||
val blockedRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
||||
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
||||
|
||||
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(false)?.use {
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
}!!
|
||||
|
||||
assertNotEquals(0, results.size)
|
||||
assertFalse(blockedRecipient in results)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABlockedRecipient_whenIQuerySignalContacts_thenIDoNotExpectBlockedToBeReturned() {
|
||||
val blockedRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
||||
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
||||
|
||||
val results = SignalDatabase.recipients.querySignalContacts("Blocked", false)!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABlockedRecipient_whenIQueryNonGroupContacts_thenIDoNotExpectBlockedToBeReturned() {
|
||||
val blockedRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
||||
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
||||
|
||||
val results = SignalDatabase.recipients.queryNonGroupContacts("Blocked", false)!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABlockedRecipient_whenIGetNonGroupContacts_thenIDoNotExpectBlockedToBeReturned() {
|
||||
val blockedRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
||||
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
||||
|
||||
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(false)?.use {
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
}!!
|
||||
|
||||
assertNotEquals(0, results.size)
|
||||
assertFalse(blockedRecipient in results)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
|
||||
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
|
||||
SignalDatabase.recipients.markUnregistered(mainId)
|
||||
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
|
||||
assertEquals(mainId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
assertNotEquals(byAci, byE164)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
|
||||
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
val mainRecord = SignalDatabase.recipients.getRecord(mainId)
|
||||
|
||||
SignalDatabase.recipients.splitForStorageSync(mainRecord.storageId!!)
|
||||
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
|
||||
assertEquals(mainId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
assertNotEquals(byAci, byE164)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACI_A = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val PNI_A = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
|
||||
const val E164_A = "+12222222222"
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* These are tests for the wrapper we wrote around SQLCipherDatabase, not the stock or SQLCipher one.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SQLiteDatabaseTest {
|
||||
|
||||
private lateinit var db: SQLiteDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.instance!!.signalWritableDatabase
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsImmediatelyIfNotInTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAfterSuccessIfInTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_doesNotRunAfterFailedTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.endTransaction()
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
// Verifying we still don't run it even after a subsequent success
|
||||
db.beginTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_onlyRunAfterAllTransactionsComplete() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.beginTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsImmediatelyIfTheTransactionIsOnAnotherThread() {
|
||||
db.beginTransaction()
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
db.runPostSuccessfulTransaction { hasRun.set(true) }
|
||||
assertTrue(hasRun.get())
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
latch.await()
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAfterSuccessIfInTransaction_ignoreDuplicates() {
|
||||
val hasRun1 = AtomicBoolean(false)
|
||||
val hasRun2 = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction("key") { hasRun1.set(true) }
|
||||
db.runPostSuccessfulTransaction("key") { hasRun2.set(true) }
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
try {
|
||||
db.beginTransaction()
|
||||
hasRun.set(true)
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransactionAndRunPostNested() {
|
||||
val hasRun1 = AtomicBoolean(false)
|
||||
val hasRun2 = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
hasRun2.set(true)
|
||||
}
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
hasRun1.set(true)
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun1.get())
|
||||
assertTrue(hasRun2.get())
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupChange
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName", "TestFunctionName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
|
||||
private lateinit var recipients: RecipientTable
|
||||
private lateinit var sms: MessageTable
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
private var wallClock: Long = 1000
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipients = SignalDatabase.recipients
|
||||
sms = SignalDatabase.messages
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
|
||||
bob = recipients.getOrInsertFromServiceId(bobServiceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if no previous messages.
|
||||
*/
|
||||
@Test
|
||||
fun noPreviousMessage() {
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
1,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous message is text.
|
||||
*/
|
||||
@Test
|
||||
fun previousTextMesssage() {
|
||||
val threadId = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous is unrelated group change.
|
||||
*/
|
||||
@Test
|
||||
fun previousUnrelatedGroupChange() {
|
||||
val threadId = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addMember(bobServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous join request is from a different recipient.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestFromADifferentRecipient() {
|
||||
val threadId = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = bob,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = bobServiceId) {
|
||||
deleteRequestingMember(bobServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse if previous is join request from same.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestCollapse() {
|
||||
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse if previous is join request from same, and leave second previous alone if text.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinThenTextCollapse() {
|
||||
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
|
||||
|
||||
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
assert(secondLatestMessage.threadId == latestMessage.threadId)
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse "twice" is previous is a join request and second previous is already collapsed join/delete from the same recipient.
|
||||
*/
|
||||
@Test
|
||||
fun previousCollapseAndJoinRequestDoubleCollapse() {
|
||||
val secondLatestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
assert(secondLatestMessage.threadId == latestMessage.threadId)
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as second latest message", result.get().messageId, `is`(secondLatestMessage.messageId))
|
||||
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingTextMessage {
|
||||
wallClock++
|
||||
return IncomingTextMessage(sender, 1, wallClock, wallClock, wallClock, body, Optional.of(groupId), 0, true, null)
|
||||
}
|
||||
|
||||
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingGroupUpdateMessage {
|
||||
return IncomingGroupUpdateMessage(smsMessage(sender, null), groupContext)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
private val groupId = GroupId.v2(masterKey)
|
||||
}
|
||||
}
|
||||
@@ -1,466 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.TestCase.assertNull
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.containsInAnyOrder
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StorySendTableTest {
|
||||
|
||||
private val distributionId1 = DistributionId.from(UUID.randomUUID())
|
||||
private val distributionId2 = DistributionId.from(UUID.randomUUID())
|
||||
private val distributionId3 = DistributionId.from(UUID.randomUUID())
|
||||
|
||||
private lateinit var distributionList1: DistributionListId
|
||||
private lateinit var distributionList2: DistributionListId
|
||||
private lateinit var distributionList3: DistributionListId
|
||||
|
||||
private lateinit var distributionListRecipient1: Recipient
|
||||
private lateinit var distributionListRecipient2: Recipient
|
||||
private lateinit var distributionListRecipient3: Recipient
|
||||
|
||||
private lateinit var recipients1to10: List<RecipientId>
|
||||
private lateinit var recipients11to20: List<RecipientId>
|
||||
private lateinit var recipients6to15: List<RecipientId>
|
||||
private lateinit var recipients6to10: List<RecipientId>
|
||||
|
||||
private var messageId1: Long = 0
|
||||
private var messageId2: Long = 0
|
||||
private var messageId3: Long = 0
|
||||
|
||||
private lateinit var storySends: StorySendTable
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
storySends = SignalDatabase.storySends
|
||||
|
||||
recipients1to10 = makeRecipients(10)
|
||||
recipients11to20 = makeRecipients(10)
|
||||
|
||||
distributionList1 = SignalDatabase.distributionLists.createList("1", emptyList(), distributionId = distributionId1)!!
|
||||
distributionList2 = SignalDatabase.distributionLists.createList("2", emptyList(), distributionId = distributionId2)!!
|
||||
distributionList3 = SignalDatabase.distributionLists.createList("3", emptyList(), distributionId = distributionId3)!!
|
||||
|
||||
distributionListRecipient1 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList1))
|
||||
distributionListRecipient2 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList2))
|
||||
distributionListRecipient3 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList3))
|
||||
|
||||
messageId1 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient1,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES
|
||||
)
|
||||
|
||||
messageId2 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
messageId3 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient3,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES
|
||||
)
|
||||
|
||||
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
|
||||
recipients6to10 = recipients1to10.takeLast(5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients6to15, 100, true, distributionId2)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(5))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapAll() {
|
||||
val recipient1 = recipients1to10.first()
|
||||
val recipient2 = recipients11to20.first()
|
||||
|
||||
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false, distributionId1)
|
||||
storySends.insert(messageId2, listOf(recipient1), 100, true, distributionId2)
|
||||
storySends.insert(messageId3, listOf(recipient2), 100, true, distributionId3)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
val recipientIdsForMessage3 = storySends.getRecipientsToSendTo(messageId3, 100, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(0))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(1))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(recipient1))
|
||||
|
||||
assertThat(recipientIdsForMessage3, hasSize(1))
|
||||
assertThat(recipientIdsForMessage3, containsInAnyOrder(recipient2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
|
||||
storySends.insert(messageId1, recipients6to15, 100, true, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 100, false, distributionId2)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(5))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(5))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(5))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.takeLast(5).toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
SignalDatabase.messages.markAsRemoteDelete(messageId1)
|
||||
|
||||
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
|
||||
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithReplies() {
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
assertThat(canReply, `is`(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithoutReplies() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
assertThat(canReply, `is`(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithAndWithoutRepliesOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients6to10, 200, true, distributionId2)
|
||||
|
||||
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
|
||||
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
|
||||
|
||||
assertThat(message1OnlyRecipientCanReply, `is`(false))
|
||||
assertThat(message2RecipientCanReply, `is`(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASingleStory_whenIGetFullSentStorySyncManifest_thenIExpectNotNull() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
|
||||
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)
|
||||
|
||||
assertNotNull(manifest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNull() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 200, false, distributionId2)
|
||||
|
||||
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)
|
||||
|
||||
assertNull(manifest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectOneManifestPerRecipient() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
|
||||
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
|
||||
|
||||
assertEquals(recipients1to10, manifest.entries.map { it.recipientId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectTwoListsPerRecipient() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
|
||||
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
|
||||
|
||||
manifest.entries.forEach { entry ->
|
||||
assertEquals(listOf(distributionId1, distributionId2), entry.distributionLists)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectAllRecipientsCanReply() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
|
||||
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
|
||||
|
||||
manifest.entries.forEach { entry ->
|
||||
assertTrue(entry.allowedToReply)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNonNullResult() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
SignalDatabase.messages.markAsRemoteDelete(messageId1)
|
||||
|
||||
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)!!
|
||||
|
||||
assertNotNull(manifest)
|
||||
}
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetRecipientIdsForManifestUpdate_thenIExpectOnlyRecipientsWithStory2() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId1, recipients11to20, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
SignalDatabase.messages.markAsRemoteDelete(messageId1)
|
||||
|
||||
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
|
||||
|
||||
assertEquals(recipients1to10.toHashSet(), recipientIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectOnlyRecipientsThatHadStory1() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
|
||||
SignalDatabase.messages.markAsRemoteDelete(messageId1)
|
||||
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
|
||||
|
||||
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
|
||||
|
||||
val manifestRecipients = results.entries.map { it.recipientId }
|
||||
assertEquals(recipients1to10, manifestRecipients)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoStoriesAndTheOneThatAllowedRepliesIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectAllowRepliesToBeTrue() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
SignalDatabase.messages.markAsRemoteDelete(messageId2)
|
||||
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
|
||||
|
||||
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
|
||||
|
||||
assertTrue(results.entries.all { it.allowedToReply })
|
||||
}
|
||||
*/
|
||||
@Test
|
||||
fun givenEmptyManifest_whenIApplyRemoteManifest_thenNothingChanges() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
val expected = storySends.getFullSentStorySyncManifest(messageId1, 200)
|
||||
val emptyManifest = SentStorySyncManifest(emptyList())
|
||||
|
||||
storySends.applySentStoryManifest(emptyManifest, 200)
|
||||
val result = storySends.getFullSentStorySyncManifest(messageId1, 200)
|
||||
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnIdenticalManifest_whenIApplyRemoteManifest_thenNothingChanges() {
|
||||
val messageId4 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient1,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||
sentTimeMillis = 200
|
||||
)
|
||||
|
||||
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
|
||||
val expected = storySends.getFullSentStorySyncManifest(messageId4, 200)
|
||||
|
||||
storySends.applySentStoryManifest(expected!!, 200)
|
||||
val result = storySends.getFullSentStorySyncManifest(messageId4, 200)
|
||||
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test(expected = NoSuchMessageException::class)
|
||||
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeDeleted() {
|
||||
val messageId4 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient1,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||
sentTimeMillis = 200
|
||||
)
|
||||
|
||||
val messageId5 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient2,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||
sentTimeMillis = 200
|
||||
)
|
||||
|
||||
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
|
||||
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
|
||||
|
||||
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
|
||||
|
||||
storySends.applySentStoryManifest(remote, 200)
|
||||
|
||||
SignalDatabase.messages.getMessageRecord(messageId5)
|
||||
fail("Expected messageId5 to no longer exist.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectSharedMessageToNotBeMarkedRemoteDeleted() {
|
||||
val messageId4 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient1,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||
sentTimeMillis = 200
|
||||
)
|
||||
|
||||
val messageId5 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient2,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||
sentTimeMillis = 200
|
||||
)
|
||||
|
||||
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
|
||||
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
|
||||
|
||||
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
|
||||
|
||||
storySends.applySentStoryManifest(remote, 200)
|
||||
|
||||
assertFalse(SignalDatabase.messages.getMessageRecord(messageId4).isRemoteDelete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoLocalEntries_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatch() {
|
||||
val messageId4 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient1,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||
sentTimeMillis = 2000
|
||||
)
|
||||
|
||||
val remote = SentStorySyncManifest(
|
||||
recipients1to10.map {
|
||||
SentStorySyncManifest.Entry(
|
||||
recipientId = it,
|
||||
allowedToReply = true,
|
||||
distributionLists = listOf(distributionId1)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
storySends.applySentStoryManifest(remote, 2000)
|
||||
|
||||
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
|
||||
assertEquals(remote, local)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNonStoryMessageAtSentTimestamp_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatchAndNoCrashes() {
|
||||
val messageId4 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient1,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||
sentTimeMillis = 2000
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients1to10.first()),
|
||||
sentTimeMillis = 2000
|
||||
)
|
||||
|
||||
val remote = SentStorySyncManifest(
|
||||
recipients1to10.map {
|
||||
SentStorySyncManifest.Entry(
|
||||
recipientId = it,
|
||||
allowedToReply = true,
|
||||
distributionLists = listOf(distributionId1)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
storySends.applySentStoryManifest(remote, 2000)
|
||||
|
||||
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
|
||||
assertEquals(remote, local)
|
||||
}
|
||||
|
||||
private fun makeRecipients(count: Int): List<RecipientId> {
|
||||
return (1..count).map {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class ThreadTableTest_pinned {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule()
|
||||
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIDoNotDeleteOrUnpinTheThread() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.pinConversations(listOf(threadId))
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.messages.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
val pinned = SignalDatabase.threads.getPinnedThreadIds()
|
||||
assertTrue(threadId in pinned)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectTheThreadInUnarchivedCount() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.pinConversations(listOf(threadId))
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.messages.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
val unarchivedCount = SignalDatabase.threads.getUnarchivedConversationListCount(ConversationFilter.OFF)
|
||||
assertEquals(1, unarchivedCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectPinnedThreadInUnarchivedList() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.pinConversations(listOf(threadId))
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.messages.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.threads.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 1).use {
|
||||
it.moveToFirst()
|
||||
assertEquals(threadId, CursorUtil.requireLong(it, ThreadTable.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ThreadTableTest_recents {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule()
|
||||
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARecentRecipient_whenIBlockAndGetRecents_thenIDoNotExpectToSeeThatRecipient() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, true)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.recipients.setBlocked(recipient.id, true)
|
||||
val results: MutableList<RecipientId> = SignalDatabase.threads.getRecentConversationList(10, false, false, false, false, false, false).use { cursor ->
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (cursor.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
}
|
||||
|
||||
// THEN
|
||||
assertFalse(recipient.id in results)
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.audio.AudioHash
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
|
||||
object UriAttachmentBuilder {
|
||||
fun build(
|
||||
id: Long,
|
||||
uri: Uri = Uri.parse("content://$id"),
|
||||
contentType: String,
|
||||
transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
size: Long = 0L,
|
||||
fileName: String = "file$id",
|
||||
voiceNote: Boolean = false,
|
||||
borderless: Boolean = false,
|
||||
videoGif: Boolean = false,
|
||||
quote: Boolean = false,
|
||||
caption: String? = null,
|
||||
stickerLocator: StickerLocator? = null,
|
||||
blurHash: BlurHash? = null,
|
||||
audioHash: AudioHash? = null,
|
||||
transformProperties: AttachmentTable.TransformProperties? = null
|
||||
): UriAttachment {
|
||||
return UriAttachment(
|
||||
uri,
|
||||
contentType,
|
||||
transferState,
|
||||
size,
|
||||
fileName,
|
||||
voiceNote,
|
||||
borderless,
|
||||
videoGif,
|
||||
quote,
|
||||
caption,
|
||||
stickerLocator,
|
||||
blurHash,
|
||||
audioHash,
|
||||
transformProperties
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.thoughtcrime.securesms.database.DistributionListTables
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MyStoryMigrationTest {
|
||||
|
||||
@get:Rule val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
|
||||
|
||||
@Test
|
||||
fun givenAValidMyStory_whenIMigrate_thenIExpectMyStoryToBeValid() {
|
||||
// GIVEN
|
||||
assertValidMyStoryExists()
|
||||
|
||||
// WHEN
|
||||
runMigration()
|
||||
|
||||
// THEN
|
||||
assertValidMyStoryExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() {
|
||||
// GIVEN
|
||||
deleteMyStory()
|
||||
|
||||
// WHEN
|
||||
runMigration()
|
||||
|
||||
// THEN
|
||||
assertValidMyStoryExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenA00000000DistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() {
|
||||
// GIVEN
|
||||
setMyStoryDistributionId("0000-0000")
|
||||
|
||||
// WHEN
|
||||
runMigration()
|
||||
|
||||
// THEN
|
||||
assertValidMyStoryExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARandomDistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() {
|
||||
// GIVEN
|
||||
setMyStoryDistributionId(UUID.randomUUID().toString())
|
||||
|
||||
// WHEN
|
||||
runMigration()
|
||||
|
||||
// THEN
|
||||
assertValidMyStoryExists()
|
||||
}
|
||||
|
||||
private fun setMyStoryDistributionId(serializedId: String) {
|
||||
SignalDatabase.rawDatabase.update(
|
||||
DistributionListTables.LIST_TABLE_NAME,
|
||||
contentValuesOf(
|
||||
DistributionListTables.DISTRIBUTION_ID to serializedId
|
||||
),
|
||||
"_id = ?",
|
||||
SqlUtil.buildArgs(DistributionListId.MY_STORY)
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteMyStory() {
|
||||
SignalDatabase.rawDatabase.delete(
|
||||
DistributionListTables.LIST_TABLE_NAME,
|
||||
"_id = ?",
|
||||
SqlUtil.buildArgs(DistributionListId.MY_STORY)
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertValidMyStoryExists() {
|
||||
SignalDatabase.rawDatabase.query(
|
||||
DistributionListTables.LIST_TABLE_NAME,
|
||||
SqlUtil.COUNT,
|
||||
"_id = ? AND ${DistributionListTables.DISTRIBUTION_ID} = ?",
|
||||
SqlUtil.buildArgs(DistributionListId.MY_STORY, DistributionId.MY_STORY.toString()),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use {
|
||||
if (it.moveToNext()) {
|
||||
val count = it.getInt(0)
|
||||
assertEquals("assertValidMyStoryExists: Query produced an unexpected count.", 1, count)
|
||||
} else {
|
||||
fail("assertValidMyStoryExists: Query did not produce a count.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runMigration() {
|
||||
V151_MyStoryMigration.migrate(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
|
||||
SignalDatabase.rawDatabase,
|
||||
0,
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import android.app.Application
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import okio.ByteString
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.KbsEnclave
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
|
||||
import org.thoughtcrime.securesms.push.SignalServiceTrustStore
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.Verb
|
||||
import org.thoughtcrime.securesms.testing.runSync
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.whispersystems.signalservice.api.KeyBackupService
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.push.TrustStore
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl
|
||||
import java.security.KeyStore
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Dependency provider used for instrumentation tests (aka androidTests).
|
||||
*
|
||||
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess] and
|
||||
* [KeyBackupService].
|
||||
*/
|
||||
class InstrumentationApplicationDependencyProvider(application: Application, default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
|
||||
|
||||
private val serviceTrustStore: TrustStore
|
||||
private val uncensoredConfiguration: SignalServiceConfiguration
|
||||
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
|
||||
private val keyBackupService: KeyBackupService
|
||||
private val recipientCache: LiveRecipientCache
|
||||
|
||||
init {
|
||||
runSync {
|
||||
webServer = MockWebServer()
|
||||
baseUrl = webServer.url("").toString()
|
||||
|
||||
addMockWebRequestHandlers(
|
||||
Get("/v1/websocket/?login=") {
|
||||
MockResponse().success().withWebSocketUpgrade(mockIdentifiedWebSocket)
|
||||
},
|
||||
Get("/v1/websocket", { !it.path.contains("login") }) {
|
||||
MockResponse().success().withWebSocketUpgrade(object : WebSocketListener() {})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
webServer.setDispatcher(object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val handler = handlers.firstOrNull { it.requestPredicate(request) }
|
||||
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
|
||||
}
|
||||
})
|
||||
|
||||
serviceTrustStore = SignalServiceTrustStore(application)
|
||||
uncensoredConfiguration = SignalServiceConfiguration(
|
||||
arrayOf(SignalServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
mapOf(
|
||||
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
|
||||
),
|
||||
arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
emptyList(),
|
||||
Optional.of(SignalServiceNetworkAccess.DNS),
|
||||
Optional.empty(),
|
||||
Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS)
|
||||
)
|
||||
|
||||
serviceNetworkAccessMock = mock {
|
||||
on { getConfiguration() } doReturn uncensoredConfiguration
|
||||
on { getConfiguration(any()) } doReturn uncensoredConfiguration
|
||||
on { uncensoredConfiguration } doReturn uncensoredConfiguration
|
||||
}
|
||||
|
||||
keyBackupService = mock()
|
||||
|
||||
recipientCache = LiveRecipientCache(application) { r -> r.run() }
|
||||
}
|
||||
|
||||
override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess {
|
||||
return serviceNetworkAccessMock
|
||||
}
|
||||
|
||||
override fun provideKeyBackupService(signalServiceAccountManager: SignalServiceAccountManager, keyStore: KeyStore, enclave: KbsEnclave): KeyBackupService {
|
||||
return keyBackupService
|
||||
}
|
||||
|
||||
override fun provideRecipientCache(): LiveRecipientCache {
|
||||
return recipientCache
|
||||
}
|
||||
|
||||
class MockWebSocket : WebSocketListener() {
|
||||
private val TAG = "MockWebSocket"
|
||||
|
||||
var webSocket: WebSocket? = null
|
||||
private set
|
||||
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
Log.i(TAG, "onOpen(${webSocket.hashCode()})")
|
||||
this.webSocket = webSocket
|
||||
}
|
||||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
Log.i(TAG, "onClosing(${webSocket.hashCode()}): $code, $reason")
|
||||
this.webSocket = null
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
Log.i(TAG, "onClosed(${webSocket.hashCode()}): $code, $reason")
|
||||
this.webSocket = null
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
Log.w(TAG, "onFailure(${webSocket.hashCode()})", t)
|
||||
this.webSocket = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var webServer: MockWebServer
|
||||
private set
|
||||
lateinit var baseUrl: String
|
||||
private set
|
||||
|
||||
val mockIdentifiedWebSocket = MockWebSocket()
|
||||
|
||||
private val handlers: MutableList<Verb> = mutableListOf()
|
||||
|
||||
fun addMockWebRequestHandlers(vararg verbs: Verb) {
|
||||
handlers.addAll(verbs)
|
||||
}
|
||||
|
||||
fun injectWebSocketMessage(value: ByteString) {
|
||||
mockIdentifiedWebSocket.webSocket!!.send(value)
|
||||
}
|
||||
|
||||
fun clearHandlers() {
|
||||
handlers.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.Put
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.testing.parsedRequestBody
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyStatus
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PreKeysSyncJobTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private val aciPreKeyMeta: PreKeyMetadataStore
|
||||
get() = SignalStore.account().aciPreKeys
|
||||
|
||||
private val pniPreKeyMeta: PreKeyMetadataStore
|
||||
get() = SignalStore.account().pniPreKeys
|
||||
|
||||
private lateinit var job: PreKeysSyncJob
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
job = PreKeysSyncJob()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create signed prekeys for both identities when both do not have registered prekeys according
|
||||
* to our local state.
|
||||
*/
|
||||
@Test
|
||||
fun runWithoutRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
aciPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
|
||||
lateinit var aciSignedPreKey: SignedPreKeyEntity
|
||||
lateinit var pniSignedPreKey: SignedPreKeyEntity
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v2/keys/signed?identity=aci") { r ->
|
||||
aciSignedPreKey = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Put("/v2/keys/signed?identity=pni") { r ->
|
||||
pniSignedPreKey = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
aciPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
|
||||
val aciVerifySignatureResult = Curve.verifySignature(
|
||||
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.publicKey,
|
||||
aciSignedPreKey.publicKey.serialize(),
|
||||
aciSignedPreKey.signature
|
||||
)
|
||||
aciVerifySignatureResult assertIs true
|
||||
|
||||
val pniVerifySignatureResult = Curve.verifySignature(
|
||||
ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.publicKey,
|
||||
pniSignedPreKey.publicKey.serialize(),
|
||||
pniSignedPreKey.signature
|
||||
)
|
||||
pniVerifySignatureResult assertIs true
|
||||
}
|
||||
|
||||
/**
|
||||
* With 100 prekeys registered for each identity, do nothing.
|
||||
*/
|
||||
@Test
|
||||
fun runWithRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIs currentPniKeyId
|
||||
}
|
||||
|
||||
/**
|
||||
* With 100 prekeys registered for ACI, but no PNI prekeys registered according to local state,
|
||||
* do nothing for ACI but create PNI prekeys and update local state.
|
||||
*/
|
||||
@Test
|
||||
fun runWithRegisteredKeysForAciIdentityOnly() {
|
||||
// GIVEN
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
|
||||
Put("/v2/keys/signed?identity=pni") { MockResponse().success() }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
|
||||
}
|
||||
|
||||
/**
|
||||
* With <10 prekeys registered for each identity, upload new.
|
||||
*/
|
||||
@Test
|
||||
fun runWithLowNumberOfRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
val currentNextAciPreKeyId = aciPreKeyMeta.nextOneTimePreKeyId
|
||||
val currentNextPniPreKeyId = pniPreKeyMeta.nextOneTimePreKeyId
|
||||
|
||||
lateinit var aciPreKeyStateRequest: PreKeyState
|
||||
lateinit var pniPreKeyStateRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(5)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(5)) },
|
||||
Put("/v2/keys/?identity=aci") { r ->
|
||||
aciPreKeyStateRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Put("/v2/keys/?identity=pni") { r ->
|
||||
pniPreKeyStateRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIsNot currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
|
||||
|
||||
aciPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextAciPreKeyId
|
||||
pniPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextPniPreKeyId
|
||||
|
||||
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.let { aciIdentityKey ->
|
||||
aciPreKeyStateRequest.identityKey assertIs aciIdentityKey
|
||||
|
||||
val verifySignatureResult = Curve.verifySignature(
|
||||
aciIdentityKey.publicKey,
|
||||
aciPreKeyStateRequest.signedPreKey.publicKey.serialize(),
|
||||
aciPreKeyStateRequest.signedPreKey.signature
|
||||
)
|
||||
verifySignatureResult assertIs true
|
||||
}
|
||||
|
||||
ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.let { pniIdentityKey ->
|
||||
pniPreKeyStateRequest.identityKey assertIs pniIdentityKey
|
||||
|
||||
val verifySignatureResult = Curve.verifySignature(
|
||||
pniIdentityKey.publicKey,
|
||||
pniPreKeyStateRequest.signedPreKey.publicKey.serialize(),
|
||||
pniPreKeyStateRequest.signedPreKey.signature
|
||||
)
|
||||
verifySignatureResult assertIs true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.libsignal.usernames.Username
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.Put
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.failure
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import org.whispersystems.util.Base64UrlSafe
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RefreshOwnProfileJob__checkUsernameIsInSyncTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
SignalStore.phoneNumberPrivacy().clearUsernameOutOfSync()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoLocalUsername_whenICheckUsernameIsInSync_thenIExpectNoFailures() {
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenLocalUsernameDoesNotMatchServerUsername_whenICheckUsernameIsInSync_thenIExpectRetry() {
|
||||
// GIVEN
|
||||
var didReserve = false
|
||||
var didConfirm = false
|
||||
val username = "hello.32"
|
||||
val serverUsername = "hello.3232"
|
||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/accounts/whoami") { r ->
|
||||
MockResponse().success(
|
||||
WhoAmIResponse().apply {
|
||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(serverUsername))
|
||||
}
|
||||
)
|
||||
},
|
||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
||||
didReserve = true
|
||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
||||
},
|
||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
||||
didConfirm = true
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
|
||||
// THEN
|
||||
assertTrue(didReserve)
|
||||
assertTrue(didConfirm)
|
||||
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenLocalAndNoServer_whenICheckUsernameIsInSync_thenIExpectRetry() {
|
||||
// GIVEN
|
||||
var didReserve = false
|
||||
var didConfirm = false
|
||||
val username = "hello.32"
|
||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/accounts/whoami") { r ->
|
||||
MockResponse().success(WhoAmIResponse())
|
||||
},
|
||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
||||
didReserve = true
|
||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
||||
},
|
||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
||||
didConfirm = true
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
|
||||
// THEN
|
||||
assertTrue(didReserve)
|
||||
assertTrue(didConfirm)
|
||||
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenLocalAndServerMatch_whenICheckUsernameIsInSync_thenIExpectNoRetry() {
|
||||
// GIVEN
|
||||
var didReserve = false
|
||||
var didConfirm = false
|
||||
val username = "hello.32"
|
||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/accounts/whoami") { r ->
|
||||
MockResponse().success(
|
||||
WhoAmIResponse().apply {
|
||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))
|
||||
}
|
||||
)
|
||||
},
|
||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
||||
didReserve = true
|
||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
||||
},
|
||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
||||
didConfirm = true
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
|
||||
// THEN
|
||||
assertFalse(didReserve)
|
||||
assertFalse(didConfirm)
|
||||
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMismatchAndReservationFails_whenICheckUsernameIsInSync_thenIExpectNoConfirm() {
|
||||
// GIVEN
|
||||
var didReserve = false
|
||||
var didConfirm = false
|
||||
val username = "hello.32"
|
||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/accounts/whoami") { r ->
|
||||
MockResponse().success(
|
||||
WhoAmIResponse().apply {
|
||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash("${username}23"))
|
||||
}
|
||||
)
|
||||
},
|
||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
||||
didReserve = true
|
||||
MockResponse().failure(418)
|
||||
},
|
||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
||||
didConfirm = true
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
|
||||
// THEN
|
||||
assertTrue(didReserve)
|
||||
assertFalse(didConfirm)
|
||||
assertTrue(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.thoughtcrime.securesms.lock;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.signal.core.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.KbsData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
@@ -15,7 +12,6 @@ import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class PinHashing_hashPin_Test {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.model.toBodyRangeList
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableUtils
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class EditMessageSyncProcessorTest {
|
||||
|
||||
companion object {
|
||||
private val IGNORE_MESSAGE_COLUMNS = listOf(
|
||||
MessageTable.DATE_RECEIVED,
|
||||
MessageTable.NOTIFIED_TIMESTAMP,
|
||||
MessageTable.REACTIONS_LAST_SEEN,
|
||||
MessageTable.NOTIFIED
|
||||
)
|
||||
|
||||
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
|
||||
AttachmentTable.UNIQUE_ID,
|
||||
AttachmentTable.TRANSFER_FILE
|
||||
)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var processorV2: MessageContentProcessorV2
|
||||
private lateinit var testResult: TestResults
|
||||
private var envelopeTimestamp: Long = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
processorV2 = MessageContentProcessorV2(harness.context)
|
||||
envelopeTimestamp = System.currentTimeMillis()
|
||||
testResult = TestResults()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textMessage() {
|
||||
var originalTimestamp = envelopeTimestamp + 200
|
||||
for (i in 1..10) {
|
||||
originalTimestamp += 400
|
||||
|
||||
val toRecipient = Recipient.resolved(harness.others[0])
|
||||
|
||||
val content = MessageContentFuzzer.fuzzTextMessage()
|
||||
val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id)
|
||||
val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
||||
.setDestinationUuid(metadata.destinationServiceId.toString())
|
||||
.setTimestamp(originalTimestamp)
|
||||
.setExpirationStartTimestamp(originalTimestamp)
|
||||
.setMessage(content.dataMessage)
|
||||
)
|
||||
).build()
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer)
|
||||
val syncTextMessage = TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(originalTimestamp),
|
||||
content = syncContent,
|
||||
metadata = metadata,
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(originalTimestamp)
|
||||
)
|
||||
|
||||
val editTimestamp = originalTimestamp + 200
|
||||
val editedContent = MessageContentFuzzer.fuzzTextMessage()
|
||||
val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
||||
.setDestinationUuid(metadata.destinationServiceId.toString())
|
||||
.setTimestamp(editTimestamp)
|
||||
.setExpirationStartTimestamp(editTimestamp)
|
||||
.setEditMessage(
|
||||
EditMessage.newBuilder()
|
||||
.setDataMessage(editedContent.dataMessage)
|
||||
.setTargetSentTimestamp(originalTimestamp)
|
||||
)
|
||||
)
|
||||
).build()
|
||||
|
||||
val syncEditMessage = TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(editTimestamp),
|
||||
content = editSyncContent,
|
||||
metadata = metadata,
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(editTimestamp)
|
||||
)
|
||||
|
||||
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
|
||||
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000)
|
||||
val originalTextMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = originalTimestamp,
|
||||
body = content.dataMessage.body,
|
||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = content.dataMessage.bodyRangesList.toBodyRangeList()
|
||||
)
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
|
||||
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(originalMessageId, true)
|
||||
if (content.dataMessage.expireTimer > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
|
||||
}
|
||||
|
||||
val editMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = editTimestamp,
|
||||
body = editedContent.dataMessage.body,
|
||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(),
|
||||
messageToEdit = originalMessageId
|
||||
)
|
||||
|
||||
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(editMessageId, true)
|
||||
|
||||
if (content.dataMessage.expireTimer > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
|
||||
}
|
||||
testResult.collectLocal()
|
||||
testResult.assert()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TestResults {
|
||||
|
||||
private lateinit var localMessages: List<List<Pair<String, String?>>>
|
||||
private lateinit var localAttachments: List<List<Pair<String, String?>>>
|
||||
|
||||
private lateinit var syncMessages: List<List<Pair<String, String?>>>
|
||||
private lateinit var syncAttachments: List<List<Pair<String, String?>>>
|
||||
|
||||
fun collectLocal() {
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
localMessages = dumpMessages()
|
||||
localAttachments = dumpAttachments()
|
||||
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fun runSync(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasSyncMessage()) {
|
||||
processorV2.process(
|
||||
envelope,
|
||||
content,
|
||||
metadata,
|
||||
serverDeliveredTimestamp,
|
||||
false
|
||||
)
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
}
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
syncMessages = dumpMessages()
|
||||
syncAttachments = dumpAttachments()
|
||||
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
SignalDatabase.rawDatabase.withinTransaction { db ->
|
||||
SignalDatabase.threads.deleteAllConversations()
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'")
|
||||
}
|
||||
}
|
||||
|
||||
fun assert() {
|
||||
syncMessages.zip(localMessages)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
|
||||
syncAttachments.zip(localAttachments)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpMessages(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(MessageTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpAttachments(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(AttachmentTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpTable(table: String): List<List<Pair<String, String?>>> {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select()
|
||||
.from(table)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Rule
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.TestProtos
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
|
||||
abstract class MessageContentProcessorTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
protected fun MessageContentProcessor.doProcess(
|
||||
messageState: MessageState = MessageState.DECRYPTED_OK,
|
||||
content: SignalServiceContent,
|
||||
exceptionMetadata: ExceptionMetadata = ExceptionMetadata("sender", 1),
|
||||
timestamp: Long = 100L,
|
||||
smsMessageId: Long = -1L
|
||||
) {
|
||||
process(messageState, content, exceptionMetadata, timestamp, smsMessageId)
|
||||
}
|
||||
|
||||
protected fun createNormalContentTestSubject(): MessageContentProcessor {
|
||||
val context = ApplicationProvider.getApplicationContext<Application>()
|
||||
|
||||
return MessageContentProcessor.create(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a valid ServiceContentProto with a data message which can be built via
|
||||
* `injectDataMessage`. This function is intended to be built on-top of for more
|
||||
* specific scenario in subclasses.
|
||||
*
|
||||
* Example can be seen in __handleStoryMessageTest
|
||||
*/
|
||||
protected fun createServiceContentWithDataMessage(
|
||||
messageSender: Recipient = Recipient.resolved(harness.others.first()),
|
||||
injectDataMessage: SignalServiceProtos.DataMessage.Builder.() -> Unit
|
||||
): SignalServiceContentProto {
|
||||
return TestProtos.build {
|
||||
serviceContent(
|
||||
localAddress = address(uuid = harness.self.requireServiceId().uuid()).build(),
|
||||
metadata = metadata(
|
||||
address = address(uuid = messageSender.requireServiceId().uuid()).build()
|
||||
).build()
|
||||
).apply {
|
||||
content = content().apply {
|
||||
dataMessage = dataMessage().apply {
|
||||
injectDataMessage()
|
||||
}.build()
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Entry
|
||||
import org.thoughtcrime.securesms.testing.InMemoryLogger
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableUtils
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import java.util.Optional
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageContentProcessorTestV2 {
|
||||
|
||||
companion object {
|
||||
private val TAGS = listOf(MessageContentProcessor.TAG, MessageContentProcessorV2.TAG, AttachmentTable.TAG)
|
||||
|
||||
private val GENERALIZE_TAG = mapOf(
|
||||
MessageContentProcessor.TAG to "MCP",
|
||||
MessageContentProcessorV2.TAG to "MCP",
|
||||
AttachmentTable.TAG to AttachmentTable.TAG
|
||||
)
|
||||
|
||||
private val IGNORE_MESSAGE_COLUMNS = listOf(
|
||||
MessageTable.DATE_RECEIVED,
|
||||
MessageTable.NOTIFIED_TIMESTAMP,
|
||||
MessageTable.REACTIONS_LAST_SEEN,
|
||||
MessageTable.NOTIFIED
|
||||
)
|
||||
|
||||
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
|
||||
AttachmentTable.UNIQUE_ID,
|
||||
AttachmentTable.TRANSFER_FILE
|
||||
)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var processorV1: MessageContentProcessor
|
||||
private lateinit var processorV2: MessageContentProcessorV2
|
||||
private lateinit var testResult: TestResults
|
||||
private var envelopeTimestamp: Long = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
processorV1 = MessageContentProcessor(harness.context)
|
||||
processorV2 = MessageContentProcessorV2(harness.context)
|
||||
envelopeTimestamp = System.currentTimeMillis()
|
||||
testResult = TestResults()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textMessage() {
|
||||
var start = envelopeTimestamp
|
||||
|
||||
val messages: List<TestMessage> = (0 until 100).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
testResult.runV2(messages)
|
||||
testResult.runV1(messages)
|
||||
|
||||
testResult.assert()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaMessage() {
|
||||
var start = envelopeTimestamp
|
||||
|
||||
val textMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val firstBatchMediaMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageWithBody(textMessages),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val secondBatchNoContentMediaMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageNoContent(textMessages + firstBatchMediaMessages),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val thirdBatchNoTextMediaMessagesMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageNoText(textMessages + firstBatchMediaMessages),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
testResult.runV2(textMessages + firstBatchMediaMessages + secondBatchNoContentMediaMessages + thirdBatchNoTextMediaMessagesMessages)
|
||||
testResult.runV1(textMessages + firstBatchMediaMessages + secondBatchNoContentMediaMessages + thirdBatchNoTextMediaMessagesMessages)
|
||||
|
||||
testResult.assert()
|
||||
}
|
||||
|
||||
private inner class TestResults {
|
||||
|
||||
private lateinit var v1Logs: List<Entry>
|
||||
private lateinit var v1Messages: List<List<Pair<String, String?>>>
|
||||
private lateinit var v1Attachments: List<List<Pair<String, String?>>>
|
||||
private lateinit var v2Logs: List<Entry>
|
||||
private lateinit var v2Messages: List<List<Pair<String, String?>>>
|
||||
private lateinit var v2Attachments: List<List<Pair<String, String?>>>
|
||||
|
||||
fun runV1(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasDataMessage()) {
|
||||
processorV1.process(
|
||||
MessageContentProcessor.MessageState.DECRYPTED_OK,
|
||||
toSignalServiceContent(envelope, content, metadata, serverDeliveredTimestamp),
|
||||
null,
|
||||
envelope.timestamp,
|
||||
-1
|
||||
)
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
v1Logs = harness.inMemoryLogger.logs()
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
v1Messages = dumpMessages()
|
||||
v1Attachments = dumpAttachments()
|
||||
}
|
||||
|
||||
fun runV2(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasDataMessage()) {
|
||||
processorV2.process(
|
||||
envelope,
|
||||
content,
|
||||
metadata,
|
||||
serverDeliveredTimestamp,
|
||||
false
|
||||
)
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
v2Logs = harness.inMemoryLogger.logs()
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
v2Messages = dumpMessages()
|
||||
v2Attachments = dumpAttachments()
|
||||
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
SignalDatabase.rawDatabase.withinTransaction { db ->
|
||||
SignalDatabase.threads.deleteAllConversations()
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'")
|
||||
}
|
||||
}
|
||||
|
||||
fun assert() {
|
||||
v2Logs.zip(v1Logs)
|
||||
.forEach { (v2, v1) ->
|
||||
GENERALIZE_TAG[v2.tag]!!.assertIs(GENERALIZE_TAG[v1.tag]!!)
|
||||
|
||||
if (v2.tag != AttachmentTable.TAG) {
|
||||
if (v2.message?.startsWith("[") == true && v1.message?.startsWith("[") == false) {
|
||||
v2.message!!.substring(v2.message!!.indexOf(']') + 2).assertIs(v1.message)
|
||||
} else {
|
||||
v2.message.assertIs(v1.message)
|
||||
}
|
||||
} else {
|
||||
if (v2.message?.startsWith("Inserted attachment at ID: AttachmentId::") == true) {
|
||||
v2.message!!
|
||||
.substring(0, v2.message!!.indexOf(','))
|
||||
.assertIs(
|
||||
v1.message!!
|
||||
.substring(0, v1.message!!.indexOf(','))
|
||||
)
|
||||
} else {
|
||||
v2.message.assertIs(v1.message)
|
||||
}
|
||||
}
|
||||
v2.throwable.assertIs(v1.throwable)
|
||||
}
|
||||
|
||||
v2Messages.zip(v1Messages)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
|
||||
v2Attachments.zip(v1Attachments)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun InMemoryLogger.logs(): List<Entry> {
|
||||
return entries()
|
||||
.filter { TAGS.contains(it.tag) }
|
||||
}
|
||||
|
||||
private fun dumpMessages(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(MessageTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpAttachments(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(AttachmentTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpTable(table: String): List<List<Pair<String, String?>>> {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select()
|
||||
.from(table)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toSignalServiceContent(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long): SignalServiceContent {
|
||||
val localAddress = SignalServiceAddress(metadata.destinationServiceId, Optional.ofNullable(SignalStore.account().e164))
|
||||
val signalServiceMetadata = SignalServiceMetadata(
|
||||
SignalServiceAddress(metadata.sourceServiceId, Optional.ofNullable(metadata.sourceE164)),
|
||||
metadata.sourceDeviceId,
|
||||
envelope.timestamp,
|
||||
envelope.serverTimestamp,
|
||||
serverDeliveredTimestamp,
|
||||
metadata.sealedSender,
|
||||
envelope.serverGuid,
|
||||
Optional.ofNullable(metadata.groupId),
|
||||
metadata.destinationServiceId.toString()
|
||||
)
|
||||
|
||||
val contentProto = SignalServiceContentProto.newBuilder()
|
||||
.setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress))
|
||||
.setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(signalServiceMetadata))
|
||||
.setContent(content)
|
||||
.build()
|
||||
|
||||
return SignalServiceContent.createFromProto(contentProto)!!
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MmsHelper
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.TestProtos
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import kotlin.random.Random
|
||||
|
||||
@Suppress("ClassName")
|
||||
class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorTest() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenContentWithADirectStoryReplyWhenIProcessThenIInsertAReplyInTheCorrectThread() {
|
||||
val sender = Recipient.resolved(harness.others.first())
|
||||
val senderThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(sender)
|
||||
val myStory = Recipient.resolved(SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!!)
|
||||
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
|
||||
val expectedSentTime = 200L
|
||||
val storyMessageId = MmsHelper.insert(
|
||||
sentTimeMillis = expectedSentTime,
|
||||
recipient = myStory,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = myStoryThread
|
||||
)
|
||||
|
||||
SignalDatabase.storySends.insert(
|
||||
messageId = storyMessageId,
|
||||
recipientIds = listOf(sender.id),
|
||||
sentTimestamp = expectedSentTime,
|
||||
allowsReplies = true,
|
||||
distributionId = DistributionId.MY_STORY
|
||||
)
|
||||
|
||||
val expectedBody = "Hello!"
|
||||
|
||||
val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext(
|
||||
messageSender = sender,
|
||||
storyAuthor = harness.self,
|
||||
storySentTimestamp = expectedSentTime
|
||||
) {
|
||||
body = expectedBody
|
||||
}
|
||||
|
||||
runTestWithContent(contentProto = storyContent)
|
||||
|
||||
val replyId = SignalDatabase.messages.getConversation(senderThreadId, 0, 1).use {
|
||||
it.moveToFirst()
|
||||
it.requireLong(MessageTable.ID)
|
||||
}
|
||||
|
||||
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
|
||||
assertEquals(ParentStoryId.DirectReply(storyMessageId).serialize(), replyRecord.parentStoryId!!.serialize())
|
||||
assertEquals(expectedBody, replyRecord.body)
|
||||
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenContentWithAGroupStoryReplyWhenIProcessThenIInsertAReplyToTheCorrectStory() {
|
||||
val sender = Recipient.resolved(harness.others[0])
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(
|
||||
listOf(
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(sender.requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.setRevision(0)
|
||||
.build()
|
||||
|
||||
val group = SignalDatabase.groups.create(
|
||||
groupMasterKey,
|
||||
decryptedGroupState
|
||||
)
|
||||
|
||||
val groupRecipient = Recipient.externalGroupExact(group)
|
||||
val threadForGroup = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
|
||||
|
||||
val insertResult = MmsHelper.insert(
|
||||
message = IncomingMediaMessage(
|
||||
from = sender.id,
|
||||
sentTimeMillis = 100L,
|
||||
serverTimeMillis = 101L,
|
||||
receivedTimeMillis = 102L,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
threadId = threadForGroup
|
||||
)
|
||||
|
||||
val expectedBody = "Hello, World!"
|
||||
val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext(
|
||||
messageSender = sender,
|
||||
storyAuthor = sender,
|
||||
storySentTimestamp = 100L
|
||||
) {
|
||||
groupV2 = TestProtos.build { groupContextV2(masterKeyBytes = groupMasterKey.serialize()).build() }
|
||||
body = expectedBody
|
||||
}
|
||||
|
||||
runTestWithContent(storyContent)
|
||||
|
||||
val replyId = SignalDatabase.messages.getStoryReplies(insertResult.get().messageId).use { cursor ->
|
||||
assertEquals(1, cursor.count)
|
||||
cursor.moveToFirst()
|
||||
cursor.requireLong(MessageTable.ID)
|
||||
}
|
||||
|
||||
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
|
||||
assertEquals(ParentStoryId.GroupReply(insertResult.get().messageId).serialize(), replyRecord.parentStoryId?.serialize())
|
||||
assertEquals(threadForGroup, replyRecord.threadId)
|
||||
assertEquals(expectedBody, replyRecord.body)
|
||||
|
||||
SignalDatabase.messages.deleteGroupStoryReplies(insertResult.get().messageId)
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ServiceContent proto with a StoryContext, and then
|
||||
* uses `injectDataMessage` to fill in the data message object.
|
||||
*/
|
||||
private fun createServiceContentWithStoryContext(
|
||||
messageSender: Recipient,
|
||||
storyAuthor: Recipient,
|
||||
storySentTimestamp: Long,
|
||||
injectDataMessage: DataMessage.Builder.() -> Unit
|
||||
): SignalServiceContentProto {
|
||||
return createServiceContentWithDataMessage(messageSender) {
|
||||
storyContext = TestProtos.build {
|
||||
storyContext(
|
||||
sentTimestamp = storySentTimestamp,
|
||||
authorUuid = storyAuthor.requireServiceId().toString()
|
||||
).build()
|
||||
}
|
||||
injectDataMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestWithContent(contentProto: SignalServiceContentProto) {
|
||||
val content = SignalServiceContent.createFromProto(contentProto)
|
||||
val testSubject = createNormalContentTestSubject()
|
||||
testSubject.doProcess(content = content!!)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
|
||||
@Suppress("ClassName")
|
||||
class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTest() {
|
||||
@Test
|
||||
fun givenContentWithATextMessageWhenIProcessThenIInsertTheTextMessage() {
|
||||
val testSubject: MessageContentProcessor = createNormalContentTestSubject()
|
||||
val expectedBody = "Hello, World!"
|
||||
val contentProto: SignalServiceContentProto = createServiceContentWithDataMessage {
|
||||
body = expectedBody
|
||||
}
|
||||
|
||||
val content = SignalServiceContent.createFromProto(contentProto)
|
||||
|
||||
// WHEN
|
||||
testSubject.doProcess(content = content!!)
|
||||
|
||||
// THEN
|
||||
val record = SignalDatabase.messages.getMessageRecord(1)
|
||||
val threadSize = SignalDatabase.messages.getMessageCountForThread(record.threadId)
|
||||
assertEquals(1, threadSize)
|
||||
|
||||
assertTrue(record.isSecure)
|
||||
assertEquals(expectedBody, record.body)
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.AliceClient
|
||||
import org.thoughtcrime.securesms.testing.BobClient
|
||||
import org.thoughtcrime.securesms.testing.Entry
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.awaitFor
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketMessage
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketRequestMessage
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import android.util.Log as AndroidLog
|
||||
|
||||
/**
|
||||
* Sends N messages from Bob to Alice to track performance of Alice's processing of messages.
|
||||
*/
|
||||
// @Ignore("Ignore test in normal testing as it's a performance test with no assertions")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageProcessingPerformanceTest {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MessageProcessingPerformanceTest::class.java)
|
||||
private val TIMING_TAG = "TIMING_$TAG".substring(0..23)
|
||||
|
||||
private val DECRYPTION_TIME_PATTERN = Pattern.compile("^Decrypted (?<count>\\d+) envelopes in (?<duration>\\d+) ms.*$")
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private val trustRoot: ECKeyPair = Curve.generateKeyPair()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic(UnidentifiedAccessUtil::class)
|
||||
every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
|
||||
|
||||
mockkObject(MessageContentProcessorV2)
|
||||
every { MessageContentProcessorV2.create(harness.application) } returns TimingMessageContentProcessorV2(harness.application)
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
unmockkStatic(UnidentifiedAccessUtil::class)
|
||||
unmockkStatic(MessageContentProcessorV2::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformance() {
|
||||
val aliceClient = AliceClient(
|
||||
serviceId = harness.self.requireServiceId(),
|
||||
e164 = harness.self.requireE164(),
|
||||
trustRoot = trustRoot
|
||||
)
|
||||
|
||||
val bob = Recipient.resolved(harness.others[0])
|
||||
val bobClient = BobClient(
|
||||
serviceId = bob.requireServiceId(),
|
||||
e164 = bob.requireE164(),
|
||||
identityKeyPair = harness.othersKeys[0],
|
||||
trustRoot = trustRoot,
|
||||
profileKey = ProfileKey(bob.profileKey)
|
||||
)
|
||||
|
||||
// Send the initial messages to get past the prekey phase
|
||||
establishSession(aliceClient, bobClient, bob)
|
||||
|
||||
// Have Bob generate N messages that will be received by Alice
|
||||
val messageCount = 100
|
||||
val envelopes = generateInboundEnvelopes(bobClient, messageCount)
|
||||
val firstTimestamp = envelopes.first().timestamp
|
||||
val lastTimestamp = envelopes.last().timestamp
|
||||
|
||||
// Inject the envelopes into the websocket
|
||||
Thread {
|
||||
for (envelope in envelopes) {
|
||||
Log.i(TIMING_TAG, "Retrieved envelope! ${envelope.timestamp}")
|
||||
InstrumentationApplicationDependencyProvider.injectWebSocketMessage(envelope.toWebSocketPayload())
|
||||
}
|
||||
InstrumentationApplicationDependencyProvider.injectWebSocketMessage(webSocketTombstone())
|
||||
}.start()
|
||||
|
||||
// Wait until they've all been fully decrypted + processed
|
||||
harness
|
||||
.inMemoryLogger
|
||||
.getLockForUntil(TimingMessageContentProcessorV2.endTagPredicate(lastTimestamp))
|
||||
.awaitFor(1.minutes)
|
||||
|
||||
harness.inMemoryLogger.flush()
|
||||
|
||||
// Process logs for timing data
|
||||
val entries = harness.inMemoryLogger.entries()
|
||||
|
||||
// Calculate decryption average
|
||||
val totalDecryptDuration: Long = entries
|
||||
.mapNotNull { entry -> entry.message?.let { DECRYPTION_TIME_PATTERN.matcher(it) } }
|
||||
.filter { it.matches() }
|
||||
.drop(1) // Ignore the first message, which represents the prekey exchange
|
||||
.sumOf { it.group("duration")!!.toLong() }
|
||||
|
||||
AndroidLog.w(TAG, "Decryption: Average runtime: ${totalDecryptDuration.toFloat() / messageCount.toFloat()}ms")
|
||||
|
||||
// Calculate MessageContentProcessor
|
||||
|
||||
val takeLast: List<Entry> = entries.filter { it.tag == TimingMessageContentProcessorV2.TAG }.drop(2)
|
||||
val iterator = takeLast.iterator()
|
||||
var processCount = 0L
|
||||
var processDuration = 0L
|
||||
while (iterator.hasNext()) {
|
||||
val start = iterator.next()
|
||||
val end = iterator.next()
|
||||
processCount++
|
||||
processDuration += end.timestamp - start.timestamp
|
||||
}
|
||||
|
||||
AndroidLog.w(TAG, "MessageContentProcessor.process: Average runtime: ${processDuration.toFloat() / processCount.toFloat()}ms")
|
||||
|
||||
// Calculate messages per second from "retrieving" first message post session initialization to processing last message
|
||||
|
||||
val start = entries.first { it.message == "Retrieved envelope! $firstTimestamp" }
|
||||
val end = entries.first { it.message == TimingMessageContentProcessorV2.endTag(lastTimestamp) }
|
||||
|
||||
val duration = (end.timestamp - start.timestamp).toFloat() / 1000f
|
||||
val messagePerSecond = messageCount.toFloat() / duration
|
||||
|
||||
AndroidLog.w(TAG, "Processing $messageCount messages took ${duration}s or ${messagePerSecond}m/s")
|
||||
}
|
||||
|
||||
private fun establishSession(aliceClient: AliceClient, bobClient: BobClient, bob: Recipient) {
|
||||
// Send message from Bob to Alice (self)
|
||||
val firstPreKeyMessageTimestamp = System.currentTimeMillis()
|
||||
val encryptedEnvelope = bobClient.encrypt(firstPreKeyMessageTimestamp)
|
||||
|
||||
val aliceProcessFirstMessageLatch = harness
|
||||
.inMemoryLogger
|
||||
.getLockForUntil(TimingMessageContentProcessorV2.endTagPredicate(firstPreKeyMessageTimestamp))
|
||||
|
||||
Thread { aliceClient.process(encryptedEnvelope, System.currentTimeMillis()) }.start()
|
||||
aliceProcessFirstMessageLatch.awaitFor(15.seconds)
|
||||
|
||||
// Send message from Alice to Bob
|
||||
val aliceNow = System.currentTimeMillis()
|
||||
bobClient.decrypt(aliceClient.encrypt(aliceNow, bob), aliceNow)
|
||||
}
|
||||
|
||||
private fun generateInboundEnvelopes(bobClient: BobClient, count: Int): List<Envelope> {
|
||||
val envelopes = ArrayList<Envelope>(count)
|
||||
var now = System.currentTimeMillis()
|
||||
for (i in 0..count) {
|
||||
envelopes += bobClient.encrypt(now)
|
||||
now += 3
|
||||
}
|
||||
|
||||
return envelopes
|
||||
}
|
||||
|
||||
private fun webSocketTombstone(): ByteString {
|
||||
return WebSocketMessage
|
||||
.newBuilder()
|
||||
.setRequest(
|
||||
WebSocketRequestMessage.newBuilder()
|
||||
.setVerb("PUT")
|
||||
.setPath("/api/v1/queue/empty")
|
||||
)
|
||||
.build()
|
||||
.toByteArray()
|
||||
.toByteString()
|
||||
}
|
||||
|
||||
private fun Envelope.toWebSocketPayload(): ByteString {
|
||||
return WebSocketMessage
|
||||
.newBuilder()
|
||||
.setType(WebSocketMessage.Type.REQUEST)
|
||||
.setRequest(
|
||||
WebSocketRequestMessage.newBuilder()
|
||||
.setVerb("PUT")
|
||||
.setPath("/api/v1/message")
|
||||
.setId(Random(System.currentTimeMillis()).nextLong())
|
||||
.addHeaders("X-Signal-Timestamp: ${this.timestamp}")
|
||||
.setBody(this.toByteString())
|
||||
)
|
||||
.build()
|
||||
.toByteArray()
|
||||
.toByteString()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
data class TestMessage(
|
||||
val envelope: SignalServiceProtos.Envelope,
|
||||
val content: SignalServiceProtos.Content,
|
||||
val metadata: EnvelopeMetadata,
|
||||
val serverDeliveredTimestamp: Long
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.testing.LogPredicate
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
class TimingMessageContentProcessorV2(context: Context) : MessageContentProcessorV2(context) {
|
||||
companion object {
|
||||
val TAG = Log.tag(TimingMessageContentProcessorV2::class.java)
|
||||
|
||||
fun endTagPredicate(timestamp: Long): LogPredicate = { entry ->
|
||||
entry.tag == TAG && entry.message == endTag(timestamp)
|
||||
}
|
||||
|
||||
private fun startTag(timestamp: Long) = "$timestamp start"
|
||||
fun endTag(timestamp: Long) = "$timestamp end"
|
||||
}
|
||||
|
||||
override fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean) {
|
||||
Log.d(TAG, startTag(envelope.timestamp))
|
||||
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent)
|
||||
Log.d(TAG, endTag(envelope.timestamp))
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.testing.FragmentScenario
|
||||
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.reactivex.rxjava3.schedulers.TestScheduler
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.testing.Put
|
||||
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsNull
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UsernameEditFragmentTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(othersCount = 10)
|
||||
|
||||
private val ioScheduler = TestScheduler()
|
||||
private val computationScheduler = TestScheduler()
|
||||
|
||||
@get:Rule
|
||||
val testSchedulerRule = RxTestSchedulerRule(
|
||||
ioTestScheduler = ioScheduler,
|
||||
computationTestScheduler = computationScheduler
|
||||
)
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUsernameCreationInRegistration() {
|
||||
val scenario = createScenario(true)
|
||||
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
|
||||
noViewFoundException.assertIsNull()
|
||||
val toolbar = view as Toolbar
|
||||
|
||||
toolbar.navigationIcon.assertIsNull()
|
||||
}
|
||||
|
||||
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
|
||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
}
|
||||
|
||||
@Ignore("Flakey espresso test.")
|
||||
@Test
|
||||
fun testUsernameCreationOutsideOfRegistration() {
|
||||
val scenario = createScenario()
|
||||
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
|
||||
noViewFoundException.assertIsNull()
|
||||
val toolbar = view as Toolbar
|
||||
|
||||
toolbar.navigationIcon.assertIsNotNull()
|
||||
}
|
||||
|
||||
onView(withText(R.string.UsernameEditFragment_username)).check(matches(isDisplayed()))
|
||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
}
|
||||
|
||||
@Ignore("Flakey espresso test.")
|
||||
@Test
|
||||
fun testNicknameUpdateHappyPath() {
|
||||
val nickname = "Spiderman"
|
||||
val discriminator = "4578"
|
||||
val username = "$nickname${UsernameState.DELIMITER}$discriminator"
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v1/accounts/username/reserved") {
|
||||
MockResponse().success(ReserveUsernameResponse(username))
|
||||
},
|
||||
Put("/v1/accounts/username/confirm") {
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
val scenario = createScenario(isInRegistration = true)
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
onView(withId(R.id.username_text)).perform(typeText(nickname))
|
||||
|
||||
computationScheduler.advanceTimeBy(501, TimeUnit.MILLISECONDS)
|
||||
computationScheduler.triggerActions()
|
||||
|
||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||
|
||||
ioScheduler.triggerActions()
|
||||
computationScheduler.triggerActions()
|
||||
|
||||
onView(withId(R.id.username_text)).perform(closeSoftKeyboard())
|
||||
onView(withId(R.id.username_done_button)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.username_done_button)).check(matches(isEnabled()))
|
||||
onView(withText(username)).check(matches(isDisplayed()))
|
||||
|
||||
onView(withId(R.id.username_done_button)).perform(click())
|
||||
|
||||
computationScheduler.triggerActions()
|
||||
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
|
||||
}
|
||||
|
||||
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> {
|
||||
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle()
|
||||
return launchFragmentInContainer(
|
||||
fragmentArgs = fragmentArgs,
|
||||
themeResId = R.style.Signal_DayNight_NoActionBar
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.schedulers.TestScheduler
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
class SafetyNumberBottomSheetRepositoryTest {
|
||||
|
||||
@get:Rule val harness = SignalActivityRule(othersCount = 10)
|
||||
|
||||
private val testScheduler = TestScheduler()
|
||||
private val subjectUnderTest = SafetyNumberBottomSheetRepository()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
RxJavaPlugins.setInitIoSchedulerHandler { testScheduler }
|
||||
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenIOnlyHave1to1Destinations_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
|
||||
val recipients = harness.others
|
||||
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey(it, false) }
|
||||
|
||||
val result = subjectUnderTest.getBuckets(recipients, destinations).test()
|
||||
|
||||
testScheduler.triggerActions()
|
||||
|
||||
result.assertValueAt(0) { map ->
|
||||
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenIOnlyHaveASingle1to1Destination_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
|
||||
// GIVEN
|
||||
val recipients = harness.others
|
||||
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey(it, false) }
|
||||
|
||||
// WHEN
|
||||
val result = subjectUnderTest.getBuckets(recipients, destination).test(1)
|
||||
testScheduler.triggerActions()
|
||||
|
||||
// THEN
|
||||
result.assertValue { map ->
|
||||
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others.take(1)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenIHaveADistributionListDestination_whenIGetBuckets_thenIOnlyHaveDistributionListDestinationWithCorrespondingMembers() {
|
||||
// GIVEN
|
||||
val distributionListMembers = harness.others.take(5)
|
||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||
|
||||
// WHEN
|
||||
val result = subjectUnderTest.getBuckets(harness.others, listOf(destinationKey)).test(1)
|
||||
testScheduler.triggerActions()
|
||||
|
||||
// THEN
|
||||
result.assertValue { map ->
|
||||
assertMatch(
|
||||
map,
|
||||
mapOf(
|
||||
SafetyNumberBucket.DistributionListBucket(distributionList, "ListA") to harness.others.take(5)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenIHaveADistributionListDestinationAndIGetBuckets_whenIRemoveFromStories_thenIOnlyHaveDistributionListDestinationWithCorrespondingMembers() {
|
||||
// GIVEN
|
||||
val distributionListMembers = harness.others.take(5)
|
||||
val toRemove = distributionListMembers.last()
|
||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
|
||||
testScheduler.triggerActions()
|
||||
|
||||
// WHEN
|
||||
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
|
||||
testSubscriber.request(1)
|
||||
testScheduler.triggerActions()
|
||||
testSubscriber.awaitCount(3)
|
||||
|
||||
// THEN
|
||||
testSubscriber.assertValueAt(2) { map ->
|
||||
assertMatch(
|
||||
map,
|
||||
mapOf(
|
||||
SafetyNumberBucket.DistributionListBucket(distributionList, "ListA") to distributionListMembers.dropLast(1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenIHaveADistributionListDestinationAndIGetBuckets_whenIRemoveAllFromStory_thenINoLongerHaveEntryForThatBucket() {
|
||||
// GIVEN
|
||||
val distributionListMembers = harness.others.take(5)
|
||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
|
||||
testScheduler.triggerActions()
|
||||
|
||||
// WHEN
|
||||
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
|
||||
testSubscriber.request(1)
|
||||
testScheduler.triggerActions()
|
||||
testSubscriber.awaitCount(3)
|
||||
|
||||
// THEN
|
||||
testSubscriber.assertValueAt(2) { map ->
|
||||
assertMatch(map, mapOf())
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertMatch(
|
||||
resultMap: Map<SafetyNumberBucket, List<SafetyNumberRecipient>>,
|
||||
idMap: Map<SafetyNumberBucket, List<RecipientId>>
|
||||
): Boolean {
|
||||
assertEquals("Result and ID Maps had different key sets", idMap.keys, resultMap.keys)
|
||||
|
||||
resultMap.forEach { (bucket, members) ->
|
||||
assertEquals("Mismatch in Bucket $bucket", idMap[bucket], members.map { it.recipient.id })
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ContactRecordProcessorTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.account().setE164(E164_SELF)
|
||||
SignalStore.account().setAci(ACI_SELF)
|
||||
SignalStore.account().setPni(PNI_SELF)
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun process_splitContact_normalSplit() {
|
||||
// GIVEN
|
||||
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
setStorageId(originalId, STORAGE_ID_A)
|
||||
|
||||
val remote1 = buildRecord(STORAGE_ID_B) {
|
||||
setServiceId(ACI_A.toString())
|
||||
setUnregisteredAtTimestamp(100)
|
||||
}
|
||||
|
||||
val remote2 = buildRecord(STORAGE_ID_C) {
|
||||
setServiceId(PNI_A.toString())
|
||||
setServicePni(PNI_A.toString())
|
||||
setServiceE164(E164_A)
|
||||
}
|
||||
|
||||
// WHEN
|
||||
val subject = ContactRecordProcessor()
|
||||
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
|
||||
|
||||
// THEN
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
|
||||
assertEquals(originalId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
assertNotEquals(byAci, byE164)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun process_splitContact_doNotSplitIfAciRecordIsRegistered() {
|
||||
// GIVEN
|
||||
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
setStorageId(originalId, STORAGE_ID_A)
|
||||
|
||||
val remote1 = buildRecord(STORAGE_ID_B) {
|
||||
setServiceId(ACI_A.toString())
|
||||
setUnregisteredAtTimestamp(0)
|
||||
}
|
||||
|
||||
val remote2 = buildRecord(STORAGE_ID_C) {
|
||||
setServiceId(PNI_A.toString())
|
||||
setServicePni(PNI_A.toString())
|
||||
setServiceE164(E164_A)
|
||||
}
|
||||
|
||||
// WHEN
|
||||
val subject = ContactRecordProcessor()
|
||||
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
|
||||
|
||||
// THEN
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
assertEquals(originalId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
assertEquals(byAci, byE164)
|
||||
}
|
||||
|
||||
private fun buildRecord(id: StorageId, applyParams: ContactRecord.Builder.() -> ContactRecord.Builder): SignalContactRecord {
|
||||
return SignalContactRecord(id, ContactRecord.getDefaultInstance().toBuilder().applyParams().build())
|
||||
}
|
||||
|
||||
private fun setStorageId(recipientId: RecipientId, storageId: StorageId) {
|
||||
SignalDatabase.rawDatabase
|
||||
.update(RecipientTable.TABLE_NAME)
|
||||
.values(RecipientTable.STORAGE_SERVICE_ID to Base64.encodeBytes(storageId.raw))
|
||||
.where("${RecipientTable.ID} = ?", recipientId)
|
||||
.run()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACI_A = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("bbbb0000-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
val ACI_SELF = ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
|
||||
|
||||
val PNI_A = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
|
||||
val PNI_B = PNI.from(UUID.fromString("bbbb1111-cd55-40bf-adda-c35a85375533"))
|
||||
val PNI_SELF = PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
|
||||
|
||||
const val E164_A = "+12222222222"
|
||||
const val E164_B = "+13333333333"
|
||||
const val E164_SELF = "+10000000000"
|
||||
|
||||
val STORAGE_ID_A: StorageId = StorageId.forContact(byteArrayOf(1, 2, 3, 4))
|
||||
val STORAGE_ID_B: StorageId = StorageId.forContact(byteArrayOf(5, 6, 7, 8))
|
||||
val STORAGE_ID_C: StorageId = StorageId.forContact(byteArrayOf(9, 10, 11, 12))
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
|
||||
/**
|
||||
* Welcome to Alice's Client.
|
||||
*
|
||||
* Alice represent the Android instrumentation test user. Unlike [BobClient] much less is needed here
|
||||
* as it can make use of the standard Signal Android App infrastructure.
|
||||
*/
|
||||
class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECKeyPair) {
|
||||
|
||||
companion object {
|
||||
val TAG = Log.tag(AliceClient::class.java)
|
||||
}
|
||||
|
||||
private val aliceSenderCertificate = FakeClientHelpers.createCertificateFor(
|
||||
trustRoot = trustRoot,
|
||||
uuid = serviceId.uuid(),
|
||||
e164 = e164,
|
||||
deviceId = 1,
|
||||
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,
|
||||
expires = 31337
|
||||
)
|
||||
|
||||
fun process(envelope: Envelope, serverDeliveredTimestamp: Long) {
|
||||
val start = System.currentTimeMillis()
|
||||
val bufferedStore = BufferedProtocolStore.create()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
|
||||
?.mapNotNull { it.run() }
|
||||
?.forEach { ApplicationDependencies.getJobManager().add(it) }
|
||||
|
||||
bufferedStore.flushToDisk()
|
||||
val end = System.currentTimeMillis()
|
||||
Log.d(TAG, "${end - start}")
|
||||
}
|
||||
|
||||
fun encrypt(now: Long, destination: Recipient): Envelope {
|
||||
return ApplicationDependencies.getSignalServiceMessageSender().getEncryptedMessage(
|
||||
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
|
||||
FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate),
|
||||
1,
|
||||
FakeClientHelpers.encryptedTextMessage(now),
|
||||
false
|
||||
).toEnvelope(now, destination.requireServiceId())
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.signal.core.util.readToSingleInt
|
||||
import org.signal.core.util.select
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.SessionBuilder
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SessionRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
|
||||
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
/**
|
||||
* Welcome to Bob's Client.
|
||||
*
|
||||
* Bob is a "fake" client that can start a session with the Android instrumentation test user (Alice).
|
||||
*
|
||||
* Bob can create a new session using a prekey bundle created from Alice's prekeys, send a message, decrypt
|
||||
* a return message from Alice, and that'll start a standard Signal session with normal keys/ratcheting.
|
||||
*/
|
||||
class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: IdentityKeyPair, val trustRoot: ECKeyPair, val profileKey: ProfileKey) {
|
||||
|
||||
private val serviceAddress = SignalServiceAddress(serviceId, e164)
|
||||
private val registrationId = KeyHelper.generateRegistrationId(false)
|
||||
private val aciStore = BobSignalServiceAccountDataStore(registrationId, identityKeyPair)
|
||||
private val senderCertificate = FakeClientHelpers.createCertificateFor(trustRoot, serviceId.uuid(), e164, 1, identityKeyPair.publicKey.publicKey, 31337)
|
||||
private val sessionLock = object : SignalSessionLock {
|
||||
private val lock = ReentrantLock()
|
||||
|
||||
override fun acquire(): SignalSessionLock.Lock {
|
||||
lock.lock()
|
||||
return SignalSessionLock.Lock { lock.unlock() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Inspired by SignalServiceMessageSender#getEncryptedMessage */
|
||||
fun encrypt(now: Long): SignalServiceProtos.Envelope {
|
||||
val envelopeContent = FakeClientHelpers.encryptedTextMessage(now)
|
||||
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
|
||||
|
||||
if (!aciStore.containsSession(getAliceProtocolAddress())) {
|
||||
val sessionBuilder = SignalSessionBuilder(sessionLock, SessionBuilder(aciStore, getAliceProtocolAddress()))
|
||||
sessionBuilder.process(getAlicePreKeyBundle())
|
||||
}
|
||||
|
||||
return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent)
|
||||
.toEnvelope(envelopeContent.content.get().dataMessage.timestamp, getAliceServiceId())
|
||||
}
|
||||
|
||||
fun decrypt(envelope: SignalServiceProtos.Envelope, serverDeliveredTimestamp: Long) {
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
|
||||
cipher.decrypt(envelope, serverDeliveredTimestamp)
|
||||
}
|
||||
|
||||
private fun getAliceServiceId(): ServiceId {
|
||||
return SignalStore.account().requireAci()
|
||||
}
|
||||
|
||||
private fun getAlicePreKeyBundle(): PreKeyBundle {
|
||||
val selfPreKeyId = SignalDatabase.rawDatabase
|
||||
.select(OneTimePreKeyTable.KEY_ID)
|
||||
.from(OneTimePreKeyTable.TABLE_NAME)
|
||||
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ?", getAliceServiceId().toString())
|
||||
.run()
|
||||
.readToSingleInt(-1)
|
||||
|
||||
val selfPreKeyRecord = SignalDatabase.oneTimePreKeys.get(getAliceServiceId(), selfPreKeyId)!!
|
||||
|
||||
val selfSignedPreKeyId = SignalDatabase.rawDatabase
|
||||
.select(SignedPreKeyTable.KEY_ID)
|
||||
.from(SignedPreKeyTable.TABLE_NAME)
|
||||
.where("${SignedPreKeyTable.ACCOUNT_ID} = ?", getAliceServiceId().toString())
|
||||
.run()
|
||||
.readToSingleInt(-1)
|
||||
|
||||
val selfSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), selfSignedPreKeyId)!!
|
||||
|
||||
return PreKeyBundle(
|
||||
SignalStore.account().registrationId,
|
||||
1,
|
||||
selfPreKeyId,
|
||||
selfPreKeyRecord.keyPair.publicKey,
|
||||
selfSignedPreKeyId,
|
||||
selfSignedPreKeyRecord.keyPair.publicKey,
|
||||
selfSignedPreKeyRecord.signature,
|
||||
getAlicePublicKey()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAliceProtocolAddress(): SignalProtocolAddress {
|
||||
return SignalProtocolAddress(SignalStore.account().requireAci().toString(), 1)
|
||||
}
|
||||
|
||||
private fun getAlicePublicKey(): IdentityKey {
|
||||
return SignalStore.account().aciIdentityKey.publicKey
|
||||
}
|
||||
|
||||
private fun getAliceProfileKey(): ProfileKey {
|
||||
return ProfileKeyUtil.getSelfProfileKey()
|
||||
}
|
||||
|
||||
private fun getAliceUnidentifiedAccess(): Optional<UnidentifiedAccess> {
|
||||
return FakeClientHelpers.getTargetUnidentifiedAccess(profileKey, getAliceProfileKey(), senderCertificate)
|
||||
}
|
||||
|
||||
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {
|
||||
private var aliceSessionRecord: SessionRecord? = null
|
||||
|
||||
override fun getIdentityKeyPair(): IdentityKeyPair = identityKeyPair
|
||||
|
||||
override fun getLocalRegistrationId(): Int = registrationId
|
||||
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
|
||||
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
|
||||
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): Boolean = false
|
||||
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) { aliceSessionRecord = record }
|
||||
override fun getSubDeviceSessions(name: String?): List<Int> = emptyList()
|
||||
override fun containsSession(address: SignalProtocolAddress?): Boolean = aliceSessionRecord != null
|
||||
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account().aciIdentityKey.publicKey
|
||||
|
||||
override fun loadPreKey(preKeyId: Int): PreKeyRecord = throw UnsupportedOperationException()
|
||||
override fun storePreKey(preKeyId: Int, record: PreKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun containsPreKey(preKeyId: Int): Boolean = throw UnsupportedOperationException()
|
||||
override fun removePreKey(preKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun loadExistingSessions(addresses: MutableList<SignalProtocolAddress>?): MutableList<SessionRecord> = throw UnsupportedOperationException()
|
||||
override fun deleteSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
|
||||
override fun deleteAllSessions(name: String?) = throw UnsupportedOperationException()
|
||||
override fun loadSignedPreKey(signedPreKeyId: Int): SignedPreKeyRecord = throw UnsupportedOperationException()
|
||||
override fun loadSignedPreKeys(): MutableList<SignedPreKeyRecord> = throw UnsupportedOperationException()
|
||||
override fun storeSignedPreKey(signedPreKeyId: Int, record: SignedPreKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun containsSignedPreKey(signedPreKeyId: Int): Boolean = throw UnsupportedOperationException()
|
||||
override fun removeSignedPreKey(signedPreKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun storeSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?, record: SenderKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun loadSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?): SenderKeyRecord = throw UnsupportedOperationException()
|
||||
override fun archiveSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
|
||||
override fun getAllAddressesWithActiveSessions(addressNames: MutableList<String>?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
||||
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
||||
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.NativeHandleGuard
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate
|
||||
import org.signal.libsignal.metadata.certificate.ServerCertificate
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.util.Base64
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
object FakeClientHelpers {
|
||||
|
||||
val noOpCertificateValidator = object : CertificateValidator(null) {
|
||||
override fun validate(certificate: SenderCertificate, validationTime: Long) = Unit
|
||||
}
|
||||
|
||||
fun createCertificateFor(trustRoot: ECKeyPair, uuid: UUID, e164: String, deviceId: Int, identityKey: ECPublicKey, expires: Long): SenderCertificate {
|
||||
val serverKey: ECKeyPair = Curve.generateKeyPair()
|
||||
NativeHandleGuard(serverKey.publicKey).use { serverPublicGuard ->
|
||||
NativeHandleGuard(trustRoot.privateKey).use { trustRootPrivateGuard ->
|
||||
val serverCertificate = ServerCertificate(Native.ServerCertificate_New(1, serverPublicGuard.nativeHandle(), trustRootPrivateGuard.nativeHandle()))
|
||||
NativeHandleGuard(identityKey).use { identityGuard ->
|
||||
NativeHandleGuard(serverCertificate).use { serverCertificateGuard ->
|
||||
NativeHandleGuard(serverKey.privateKey).use { serverPrivateGuard ->
|
||||
return SenderCertificate(Native.SenderCertificate_New(uuid.toString(), e164, deviceId, identityGuard.nativeHandle(), expires, serverCertificateGuard.nativeHandle(), serverPrivateGuard.nativeHandle()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTargetUnidentifiedAccess(myProfileKey: ProfileKey, theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): Optional<UnidentifiedAccess> {
|
||||
val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey)
|
||||
val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
|
||||
|
||||
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized)).targetUnidentifiedAccess
|
||||
}
|
||||
|
||||
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {
|
||||
val content = SignalServiceProtos.Content.newBuilder().apply {
|
||||
setDataMessage(
|
||||
SignalServiceProtos.DataMessage.newBuilder().apply {
|
||||
body = message
|
||||
timestamp = now
|
||||
}
|
||||
)
|
||||
}
|
||||
return EnvelopeContent.encrypted(content.build(), ContentHint.RESENDABLE, Optional.empty())
|
||||
}
|
||||
|
||||
fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope {
|
||||
return Envelope.newBuilder()
|
||||
.setType(Envelope.Type.valueOf(this.type))
|
||||
.setSourceDevice(1)
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 1)
|
||||
.setDestinationUuid(destination.toString())
|
||||
.setServerGuid(UUID.randomUUID().toString())
|
||||
.setContent(Base64.decode(this.content).toProtoByteString())
|
||||
.setUrgent(true)
|
||||
.setStory(false)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
typealias LogPredicate = (Entry) -> Boolean
|
||||
|
||||
/**
|
||||
* Logging implementation that holds logs in memory as they are added to be retrieve at a later time by a test.
|
||||
* Can also be used for multithreaded synchronization and waiting until certain logs are emitted before continuing
|
||||
* a test.
|
||||
*/
|
||||
class InMemoryLogger : Log.Logger() {
|
||||
|
||||
private val executor = SignalExecutors.newCachedSingleThreadExecutor("inmemory-logger", ThreadUtil.PRIORITY_BACKGROUND_THREAD)
|
||||
private val predicates = mutableListOf<LogPredicate>()
|
||||
private val logEntries = mutableListOf<Entry>()
|
||||
|
||||
override fun v(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Verbose(tag, message, t, System.currentTimeMillis()))
|
||||
override fun d(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Debug(tag, message, t, System.currentTimeMillis()))
|
||||
override fun i(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Info(tag, message, t, System.currentTimeMillis()))
|
||||
override fun w(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Warn(tag, message, t, System.currentTimeMillis()))
|
||||
override fun e(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Error(tag, message, t, System.currentTimeMillis()))
|
||||
|
||||
override fun flush() {
|
||||
val latch = CountDownLatch(1)
|
||||
executor.execute { latch.countDown() }
|
||||
latch.await()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
val latch = CountDownLatch(1)
|
||||
executor.execute {
|
||||
predicates.clear()
|
||||
logEntries.clear()
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
}
|
||||
|
||||
private fun add(entry: Entry) {
|
||||
executor.execute {
|
||||
logEntries += entry
|
||||
|
||||
val iterator = predicates.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val predicate = iterator.next()
|
||||
if (predicate(entry)) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Blocks until a snapshot of all log entries can be taken in a thread-safe way. */
|
||||
fun entries(): List<Entry> {
|
||||
val latch = CountDownLatch(1)
|
||||
var entries: List<Entry> = emptyList()
|
||||
executor.execute {
|
||||
entries = logEntries.toList()
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
return entries
|
||||
}
|
||||
|
||||
/** Returns a countdown latch that'll fire at a future point when an [Entry] is received that matches the predicate. */
|
||||
fun getLockForUntil(predicate: LogPredicate): CountDownLatch {
|
||||
val latch = CountDownLatch(1)
|
||||
executor.execute {
|
||||
predicates += { entry ->
|
||||
if (predicate(entry)) {
|
||||
latch.countDown()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
return latch
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Entry {
|
||||
val tag: String
|
||||
val message: String?
|
||||
val throwable: Throwable?
|
||||
val timestamp: Long
|
||||
}
|
||||
|
||||
data class Verbose(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
|
||||
data class Debug(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
|
||||
data class Info(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
|
||||
data class Warn(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
|
||||
data class Error(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
|
||||
@@ -1,234 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.messages.TestMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Random but deterministic fuzzer for create various message content protos.
|
||||
*/
|
||||
object MessageContentFuzzer {
|
||||
|
||||
private val mediaTypes = listOf("image/png", "image/jpeg", "image/heic", "image/heif", "image/avif", "image/webp", "image/gif", "audio/aac", "audio/*", "video/mp4", "video/*", "text/x-vcard", "text/x-signal-plain", "application/x-signal-view-once", "*/*", "application/octet-stream")
|
||||
private val emojis = listOf("😂", "❤️", "🔥", "😍", "👀", "🤔", "🙏", "👍", "🤷", "🥺")
|
||||
|
||||
private val random = Random(1)
|
||||
|
||||
/**
|
||||
* Create an [Envelope].
|
||||
*/
|
||||
fun envelope(timestamp: Long): Envelope {
|
||||
return Envelope.newBuilder()
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 5)
|
||||
.setServerGuidBytes(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create metadata to match an [Envelope].
|
||||
*/
|
||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId): EnvelopeMetadata {
|
||||
return EnvelopeMetadata(
|
||||
sourceServiceId = Recipient.resolved(source).requireServiceId(),
|
||||
sourceE164 = null,
|
||||
sourceDeviceId = 1,
|
||||
sealedSender = true,
|
||||
groupId = null,
|
||||
destinationServiceId = Recipient.resolved(destination).requireServiceId()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random text message that will contain a body but may also contain
|
||||
* - An expire timer value
|
||||
* - Bold style body ranges
|
||||
*/
|
||||
fun fuzzTextMessage(): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().run {
|
||||
body = string()
|
||||
if (random.nextBoolean()) {
|
||||
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
||||
}
|
||||
if (random.nextBoolean()) {
|
||||
addBodyRanges(
|
||||
SignalServiceProtos.BodyRange.newBuilder().run {
|
||||
start = 0
|
||||
length = 1
|
||||
style = SignalServiceProtos.BodyRange.Style.BOLD
|
||||
build()
|
||||
}
|
||||
)
|
||||
}
|
||||
build()
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that may be:
|
||||
* - A text body
|
||||
* - A text body with a quote that references an existing message
|
||||
* - A text body with a quote that references a non existing message
|
||||
* - A message with 0-2 attachment pointers and may contain a text body
|
||||
*/
|
||||
fun fuzzMediaMessageWithBody(quoteAble: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().run {
|
||||
if (random.nextBoolean()) {
|
||||
body = string()
|
||||
}
|
||||
|
||||
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
|
||||
body = string()
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().run {
|
||||
id = quoted.envelope.timestamp
|
||||
authorUuid = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
addAllAttachments(quoted.content.dataMessage.attachmentsList)
|
||||
addAllBodyRanges(quoted.content.dataMessage.bodyRangesList)
|
||||
type = DataMessage.Quote.Type.NORMAL
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) {
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().run {
|
||||
id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp)
|
||||
authorUuid = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val total = random.nextInt(1, 2)
|
||||
(0..total).forEach { _ -> addAttachments(attachmentPointer()) }
|
||||
}
|
||||
|
||||
build()
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random media message that contains no traditional media content. It may be:
|
||||
* - A reaction to a prior message
|
||||
*/
|
||||
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().run {
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val reactTo = previousMessages.random(random)
|
||||
reaction = DataMessage.Reaction.newBuilder().run {
|
||||
emoji = emojis.random(random)
|
||||
remove = false
|
||||
targetAuthorUuid = reactTo.metadata.sourceServiceId.toString()
|
||||
targetSentTimestamp = reactTo.envelope.timestamp
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
build()
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that can never contain a text body. It may be:
|
||||
* - A sticker
|
||||
*/
|
||||
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().run {
|
||||
if (random.nextFloat() < 0.9) {
|
||||
sticker = DataMessage.Sticker.newBuilder().run {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
build()
|
||||
}
|
||||
}
|
||||
build()
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random [String].
|
||||
*/
|
||||
fun string(length: Int = 10, allowNullString: Boolean = false): String {
|
||||
var string = ""
|
||||
|
||||
if (allowNullString && random.nextBoolean()) {
|
||||
return string
|
||||
}
|
||||
|
||||
for (i in 0 until length) {
|
||||
string += random.nextInt(65..90).toChar()
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random [ByteString].
|
||||
*/
|
||||
fun byteString(length: Int = 512): ByteString {
|
||||
return random.nextBytes(length).toProtoByteString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random [AttachmentPointer].
|
||||
*/
|
||||
fun attachmentPointer(): AttachmentPointer {
|
||||
return AttachmentPointer.newBuilder().run {
|
||||
cdnKey = string()
|
||||
contentType = mediaTypes.random(random)
|
||||
key = byteString()
|
||||
size = random.nextInt(1024 * 1024 * 50)
|
||||
thumbnail = byteString()
|
||||
digest = byteString()
|
||||
fileName = string()
|
||||
flags = 0
|
||||
width = random.nextInt(until = 1024)
|
||||
height = random.nextInt(until = 1024)
|
||||
caption = string(allowNullString = true)
|
||||
blurHash = string()
|
||||
uploadTimestamp = random.nextLong()
|
||||
cdnNumber = 1
|
||||
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a server delivered timestamp that is always later than the envelope and server "received" timestamp.
|
||||
*/
|
||||
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
|
||||
return envelopeTimestamp + 10
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.stub
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.protocol.util.Medium
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.test.BuildConfig
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupService
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.DeviceInfoList
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyResponse
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyResponseItem
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson
|
||||
import org.whispersystems.signalservice.internal.push.SenderCertificate
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
* Warehouse of reusable test data and mock configurations.
|
||||
*/
|
||||
object MockProvider {
|
||||
|
||||
val senderCertificate = SenderCertificate().apply { certificate = ByteArray(0) }
|
||||
|
||||
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
|
||||
backupCredentials = AuthCredentials.create("username", "password")
|
||||
}
|
||||
|
||||
val primaryOnlyDeviceList = DeviceInfoList().apply {
|
||||
devices = listOf(
|
||||
DeviceInfo().apply {
|
||||
id = 1
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val sessionMetadataJson = RegistrationSessionMetadataJson(
|
||||
id = "asdfasdfasdfasdf",
|
||||
nextCall = null,
|
||||
nextSms = null,
|
||||
nextVerificationAttempt = null,
|
||||
allowedToRequestCode = true,
|
||||
requestedInformation = emptyList(),
|
||||
verified = true
|
||||
)
|
||||
|
||||
fun createVerifyAccountResponse(aci: ServiceId, newPni: ServiceId): VerifyAccountResponse {
|
||||
return VerifyAccountResponse().apply {
|
||||
uuid = aci.toString()
|
||||
pni = newPni.toString()
|
||||
storageCapable = false
|
||||
}
|
||||
}
|
||||
|
||||
fun createWhoAmIResponse(aci: ServiceId, pni: ServiceId, e164: String): WhoAmIResponse {
|
||||
return WhoAmIResponse().apply {
|
||||
this.uuid = aci.toString()
|
||||
this.pni = pni.toString()
|
||||
this.number = e164
|
||||
}
|
||||
}
|
||||
|
||||
fun mockGetRegistrationLockStringFlow(kbsRepository: KbsRepository) {
|
||||
val tokenData: TokenData = mock {
|
||||
on { enclave } doReturn BuildConfig.KBS_ENCLAVE
|
||||
on { basicAuth } doReturn "basicAuth"
|
||||
on { triesRemaining } doReturn 10
|
||||
on { tokenResponse } doReturn TokenResponse()
|
||||
}
|
||||
|
||||
kbsRepository.stub {
|
||||
on { getToken(any() as? String) } doReturn Single.just(ServiceResponse.forResult(tokenData, 200, ""))
|
||||
}
|
||||
|
||||
val session: KeyBackupService.RestoreSession = object : KeyBackupService.RestoreSession {
|
||||
override fun hashSalt(): ByteArray = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8")
|
||||
override fun restorePin(hashedPin: HashedPin?): KbsPinData = KbsPinData(MasterKey.createNew(SecureRandom()), null)
|
||||
}
|
||||
|
||||
val kbsService = ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE)
|
||||
kbsService.stub {
|
||||
on { newRegistrationSession(any(), any()) } doReturn session
|
||||
}
|
||||
}
|
||||
|
||||
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
|
||||
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
|
||||
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())
|
||||
|
||||
val device = PreKeyResponseItem().apply {
|
||||
this.deviceId = deviceId
|
||||
registrationId = KeyHelper.generateRegistrationId(false)
|
||||
signedPreKey = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
|
||||
preKey = PreKeyEntity(oneTimePreKey.id, oneTimePreKey.keyPair.publicKey)
|
||||
}
|
||||
|
||||
return PreKeyResponse().apply {
|
||||
identityKey = identity.publicKey
|
||||
devices = listOf(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import okhttp3.mockwebserver.SocketPolicy
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
typealias ResponseFactory = (request: RecordedRequest) -> MockResponse
|
||||
typealias RequestPredicate = (request: RecordedRequest) -> Boolean
|
||||
|
||||
/**
|
||||
* Represent an HTTP verb for mocking web requests.
|
||||
*/
|
||||
sealed class Verb(val requestPredicate: RequestPredicate, val responseFactory: ResponseFactory)
|
||||
|
||||
class Get(path: String, predicate: RequestPredicate, responseFactory: ResponseFactory) : Verb(defaultRequestPredicate("GET", path, predicate), responseFactory) {
|
||||
constructor(path: String, responseFactory: ResponseFactory) : this(path, { true }, responseFactory)
|
||||
}
|
||||
|
||||
class Put(path: String, responseFactory: ResponseFactory) : Verb(defaultRequestPredicate("PUT", path), responseFactory)
|
||||
|
||||
class Post(path: String, responseFactory: ResponseFactory) : Verb(defaultRequestPredicate("POST", path), responseFactory)
|
||||
|
||||
fun MockResponse.success(response: Any? = null): MockResponse {
|
||||
return setResponseCode(200).apply {
|
||||
if (response != null) {
|
||||
setBody(JsonUtils.toJson(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MockResponse.failure(code: Int, response: Any? = null): MockResponse {
|
||||
return setResponseCode(code).apply {
|
||||
if (response != null) {
|
||||
setBody(JsonUtils.toJson(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MockResponse.connectionFailure(): MockResponse {
|
||||
return setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)
|
||||
}
|
||||
|
||||
fun MockResponse.timeout(): MockResponse {
|
||||
return setHeadersDelay(1, TimeUnit.DAYS)
|
||||
.setBodyDelay(1, TimeUnit.DAYS)
|
||||
}
|
||||
|
||||
inline fun <reified T> RecordedRequest.parsedRequestBody(): T {
|
||||
val bodyString = String(body.readByteArray())
|
||||
return JsonUtils.fromJson(bodyString, T::class.java)
|
||||
}
|
||||
|
||||
private fun defaultRequestPredicate(verb: String, path: String, predicate: RequestPredicate = { true }): RequestPredicate = { request ->
|
||||
request.method == verb && request.path.startsWith("/$path") && predicate(request)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.schedulers.TestScheduler
|
||||
import org.junit.rules.ExternalResource
|
||||
|
||||
/**
|
||||
* JUnit Rule which initialises Rx thread schedulers. If a specific
|
||||
* scheduler is not specified, it defaults to the `defaultTestScheduler`
|
||||
*/
|
||||
class RxTestSchedulerRule(
|
||||
val defaultTestScheduler: TestScheduler = TestScheduler(),
|
||||
val ioTestScheduler: TestScheduler = defaultTestScheduler,
|
||||
val computationTestScheduler: TestScheduler = defaultTestScheduler,
|
||||
val singleTestScheduler: TestScheduler = defaultTestScheduler,
|
||||
val newThreadTestScheduler: TestScheduler = defaultTestScheduler
|
||||
) : ExternalResource() {
|
||||
|
||||
override fun before() {
|
||||
RxJavaPlugins.setInitIoSchedulerHandler { ioTestScheduler }
|
||||
RxJavaPlugins.setIoSchedulerHandler { ioTestScheduler }
|
||||
|
||||
RxJavaPlugins.setInitComputationSchedulerHandler { computationTestScheduler }
|
||||
RxJavaPlugins.setComputationSchedulerHandler { computationTestScheduler }
|
||||
|
||||
RxJavaPlugins.setInitSingleSchedulerHandler { singleTestScheduler }
|
||||
RxJavaPlugins.setSingleSchedulerHandler { singleTestScheduler }
|
||||
|
||||
RxJavaPlugins.setInitNewThreadSchedulerHandler { newThreadTestScheduler }
|
||||
RxJavaPlugins.setNewThreadSchedulerHandler { newThreadTestScheduler }
|
||||
}
|
||||
|
||||
override fun after() {
|
||||
RxJavaPlugins.reset()
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.rules.ExternalResource
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.thoughtcrime.securesms.SignalInstrumentationApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Test rule to use that sets up the application in a mostly registered state. Enough so that most
|
||||
* activities should be launchable directly.
|
||||
*
|
||||
* To use: `@get:Rule val harness = SignalActivityRule()`
|
||||
*/
|
||||
class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() {
|
||||
|
||||
val application: Application = ApplicationDependencies.getApplication()
|
||||
|
||||
lateinit var context: Context
|
||||
private set
|
||||
lateinit var self: Recipient
|
||||
private set
|
||||
lateinit var others: List<RecipientId>
|
||||
private set
|
||||
lateinit var othersKeys: List<IdentityKeyPair>
|
||||
|
||||
val inMemoryLogger: InMemoryLogger
|
||||
get() = (application as SignalInstrumentationApplicationContext).inMemoryLogger
|
||||
|
||||
override fun before() {
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
self = setupSelf()
|
||||
|
||||
val setupOthers = setupOthers()
|
||||
others = setupOthers.first
|
||||
othersKeys = setupOthers.second
|
||||
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
private fun setupSelf(): Recipient {
|
||||
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
|
||||
val masterSecret = MasterSecretUtil.generateMasterSecret(application, MasterSecretUtil.UNENCRYPTED_PASSPHRASE)
|
||||
MasterSecretUtil.generateAsymmetricMasterSecret(application, masterSecret)
|
||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||
|
||||
val registrationRepository = RegistrationRepository(application)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(Put("/v2/keys") { MockResponse().success() })
|
||||
val response: ServiceResponse<VerifyResponse> = registrationRepository.registerAccount(
|
||||
RegistrationData(
|
||||
code = "123123",
|
||||
e164 = "+15555550101",
|
||||
password = Util.getSecret(18),
|
||||
registrationId = registrationRepository.registrationId,
|
||||
profileKey = registrationRepository.getProfileKey("+15555550101"),
|
||||
fcmToken = null,
|
||||
pniRegistrationId = registrationRepository.pniRegistrationId,
|
||||
recoveryPassword = "asdfasdfasdfasdf"
|
||||
),
|
||||
VerifyResponse(VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false), null, null),
|
||||
false
|
||||
).blockingGet()
|
||||
|
||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||
|
||||
SignalStore.kbsValues().optOut()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
SignalStore.settings().isMessageNotificationsEnabled = false
|
||||
|
||||
return Recipient.self()
|
||||
}
|
||||
|
||||
private fun setupOthers(): Pair<List<RecipientId>, List<IdentityKeyPair>> {
|
||||
val others = mutableListOf<RecipientId>()
|
||||
val othersKeys = mutableListOf<IdentityKeyPair>()
|
||||
|
||||
if (othersCount !in 0 until 1000) {
|
||||
throw IllegalArgumentException("$othersCount must be between 0 and 1000")
|
||||
}
|
||||
|
||||
for (i in 0 until othersCount) {
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
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.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
||||
others += recipientId
|
||||
othersKeys += otherIdentity
|
||||
}
|
||||
|
||||
return others to othersKeys
|
||||
}
|
||||
|
||||
inline fun <reified T : Activity> launchActivity(initIntent: Intent.() -> Unit = {}): ActivityScenario<T> {
|
||||
return androidx.test.core.app.launchActivity(Intent(context, T::class.java).apply(initIntent))
|
||||
}
|
||||
|
||||
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
|
||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
|
||||
}
|
||||
|
||||
fun getIdentity(recipient: Recipient): IdentityKey {
|
||||
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
|
||||
}
|
||||
|
||||
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Sets up bare-minimum to allow writing unit tests against the database,
|
||||
* including setting up the local ACI and PNI pair.
|
||||
*
|
||||
* @param deleteAllThreadsOnEachRun Run deleteAllThreads between each unit test
|
||||
*/
|
||||
class SignalDatabaseRule(
|
||||
private val deleteAllThreadsOnEachRun: Boolean = true
|
||||
) : TestWatcher() {
|
||||
|
||||
val localAci: ACI = ACI.from(UUID.randomUUID())
|
||||
val localPni: PNI = PNI.from(UUID.randomUUID())
|
||||
|
||||
override fun starting(description: Description?) {
|
||||
deleteAllThreads()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
}
|
||||
|
||||
override fun finished(description: Description?) {
|
||||
deleteAllThreads()
|
||||
}
|
||||
|
||||
private fun deleteAllThreads() {
|
||||
if (deleteAllThreadsOnEachRun) {
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import org.thoughtcrime.securesms.SignalInstrumentationApplicationContext
|
||||
|
||||
/**
|
||||
* Custom runner that replaces application with [SignalInstrumentationApplicationContext].
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class SignalTestRunner : AndroidJUnitRunner() {
|
||||
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
|
||||
return super.newApplication(cl, SignalInstrumentationApplicationContext::class.java.name, context)
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.AddressProto
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
class TestProtos private constructor() {
|
||||
fun address(
|
||||
uuid: UUID = UUID.randomUUID()
|
||||
): AddressProto.Builder {
|
||||
return AddressProto.newBuilder()
|
||||
.setUuid(ServiceId.from(uuid).toByteString())
|
||||
}
|
||||
|
||||
fun metadata(
|
||||
address: AddressProto = address().build()
|
||||
): MetadataProto.Builder {
|
||||
return MetadataProto.newBuilder()
|
||||
.setAddress(address)
|
||||
}
|
||||
|
||||
fun groupContextV2(
|
||||
revision: Int = 0,
|
||||
masterKeyBytes: ByteArray = Random.Default.nextBytes(GroupMasterKey.SIZE)
|
||||
): GroupContextV2.Builder {
|
||||
return GroupContextV2.newBuilder()
|
||||
.setRevision(revision)
|
||||
.setMasterKey(ByteString.copyFrom(masterKeyBytes))
|
||||
}
|
||||
|
||||
fun storyContext(
|
||||
sentTimestamp: Long = Random.nextLong(),
|
||||
authorUuid: String = UUID.randomUUID().toString()
|
||||
): DataMessage.StoryContext.Builder {
|
||||
return DataMessage.StoryContext.newBuilder()
|
||||
.setAuthorUuid(authorUuid)
|
||||
.setSentTimestamp(sentTimestamp)
|
||||
}
|
||||
|
||||
fun dataMessage(): DataMessage.Builder {
|
||||
return DataMessage.newBuilder()
|
||||
}
|
||||
|
||||
fun content(): SignalServiceProtos.Content.Builder {
|
||||
return SignalServiceProtos.Content.newBuilder()
|
||||
}
|
||||
|
||||
fun serviceContent(
|
||||
localAddress: AddressProto = address().build(),
|
||||
metadata: MetadataProto = metadata().build()
|
||||
): SignalServiceContentProto.Builder {
|
||||
return SignalServiceContentProto.newBuilder()
|
||||
.setLocalAddress(localAddress)
|
||||
.setMetadata(metadata)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <T> build(buildFn: TestProtos.() -> T): T {
|
||||
return TestProtos().buildFn()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.hamcrest.Matchers.not
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Run the given [runnable] on a new thread and wait for it to finish.
|
||||
*/
|
||||
fun runSync(runnable: () -> Unit) {
|
||||
val lock = CountDownLatch(1)
|
||||
Thread {
|
||||
try {
|
||||
runnable.invoke()
|
||||
} finally {
|
||||
lock.countDown()
|
||||
}
|
||||
}.start()
|
||||
lock.await()
|
||||
}
|
||||
|
||||
/* Various kotlin-ifications of hamcrest matchers */
|
||||
|
||||
fun <T : Any?> T.assertIsNull() {
|
||||
assertThat(this, nullValue())
|
||||
}
|
||||
|
||||
fun <T : Any?> T.assertIsNotNull() {
|
||||
assertThat(this, notNullValue())
|
||||
}
|
||||
|
||||
infix fun <T : Any?> T.assertIs(expected: T) {
|
||||
assertThat(this, `is`(expected))
|
||||
}
|
||||
|
||||
infix fun <T : Any> T.assertIsNot(expected: T) {
|
||||
assertThat(this, not(`is`(expected)))
|
||||
}
|
||||
|
||||
infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
|
||||
assertThat(this, hasSize(expected))
|
||||
}
|
||||
|
||||
fun CountDownLatch.awaitFor(duration: Duration) {
|
||||
if (!await(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS)) {
|
||||
throw TimeoutException("Latch await took longer than ${duration.inWholeMilliseconds}ms")
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
/**
|
||||
* A class that allows us to inject feature flags during tests.
|
||||
*/
|
||||
public final class FeatureFlagsAccessor {
|
||||
|
||||
public static void forceValue(String key, Object value) {
|
||||
FeatureFlags.FORCED_VALUES.put(FeatureFlags.PHONE_NUMBER_PRIVACY, true);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
|
||||
object MessageTableUtils {
|
||||
fun typeColumnToString(type: Long): String {
|
||||
return """
|
||||
isOutgoingMessageType:${MessageTypes.isOutgoingMessageType(type)}
|
||||
isForcedSms:${type and MessageTypes.MESSAGE_FORCE_SMS_BIT != 0L}
|
||||
isDraftMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_DRAFT_TYPE}
|
||||
isFailedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_FAILED_TYPE}
|
||||
isPendingMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_OUTBOX_TYPE || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENDING_TYPE}
|
||||
isSentType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_TYPE}
|
||||
isPendingSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
|
||||
isPendingSecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
|
||||
isPendingInsecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK}
|
||||
isInboxType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_INBOX_TYPE}
|
||||
isJoinedType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.JOINED_TYPE}
|
||||
isUnsupportedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.UNSUPPORTED_MESSAGE_TYPE}
|
||||
isInvalidMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.INVALID_MESSAGE_TYPE}
|
||||
isBadDecryptType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BAD_DECRYPT_TYPE}
|
||||
isSecureType:${type and MessageTypes.SECURE_MESSAGE_BIT != 0L}
|
||||
isPushType:${type and MessageTypes.PUSH_MESSAGE_BIT != 0L}
|
||||
isEndSessionType:${type and MessageTypes.END_SESSION_BIT != 0L}
|
||||
isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L}
|
||||
isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
|
||||
isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
|
||||
isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L}
|
||||
isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
|
||||
isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L}
|
||||
isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L}
|
||||
isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
|
||||
isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L}
|
||||
isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L}
|
||||
isIncomingAudioCall:${type == MessageTypes.INCOMING_AUDIO_CALL_TYPE}
|
||||
isIncomingVideoCall:${type == MessageTypes.INCOMING_VIDEO_CALL_TYPE}
|
||||
isOutgoingAudioCall:${type == MessageTypes.OUTGOING_AUDIO_CALL_TYPE}
|
||||
isOutgoingVideoCall:${type == MessageTypes.OUTGOING_VIDEO_CALL_TYPE}
|
||||
isMissedAudioCall:${type == MessageTypes.MISSED_AUDIO_CALL_TYPE}
|
||||
isMissedVideoCall:${type == MessageTypes.MISSED_VIDEO_CALL_TYPE}
|
||||
isGroupCall:${type == MessageTypes.GROUP_CALL_TYPE}
|
||||
isGroupUpdate:${type and MessageTypes.GROUP_UPDATE_BIT != 0L}
|
||||
isGroupV2:${type and MessageTypes.GROUP_V2_BIT != 0L}
|
||||
isGroupQuit:${type and MessageTypes.GROUP_LEAVE_BIT != 0L && type and MessageTypes.GROUP_V2_BIT == 0L}
|
||||
isChatSessionRefresh:${type and MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT != 0L}
|
||||
isDuplicateMessageType:${type and MessageTypes.ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L}
|
||||
isDecryptInProgressType:${type and 0x40000000 != 0L}
|
||||
isNoRemoteSessionType:${type and MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L}
|
||||
isLegacyType:${type and MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and MessageTypes.ENCRYPTION_REMOTE_BIT != 0L}
|
||||
isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE}
|
||||
isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE}
|
||||
isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE}
|
||||
isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE}
|
||||
isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE}
|
||||
isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE}
|
||||
isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS}
|
||||
isSpecialType:${type and MessageTypes.SPECIAL_TYPES_MASK != 0L}
|
||||
isStoryReaction:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_STORY_REACTION}
|
||||
isGiftBadge:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_GIFT_BADGE}
|
||||
isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION}
|
||||
isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST}
|
||||
isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED}
|
||||
""".trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "")
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<profileable android:shell="true" />
|
||||
|
||||
<activity android:name="org.signal.benchmark.BenchmarkSetupActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.signal.benchmark
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import org.signal.benchmark.setup.TestMessages
|
||||
import org.signal.benchmark.setup.TestUsers
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class BenchmarkSetupActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
when (intent.extras!!.getString("setup-type")) {
|
||||
"cold-start" -> setupColdStart()
|
||||
"conversation-open" -> setupConversationOpen()
|
||||
}
|
||||
|
||||
val textView: TextView = TextView(this).apply {
|
||||
text = "done"
|
||||
}
|
||||
setContentView(textView)
|
||||
}
|
||||
|
||||
private fun setupColdStart() {
|
||||
TestUsers.setupSelf()
|
||||
TestUsers.setupTestRecipients(50).forEach {
|
||||
val recipient: Recipient = Recipient.resolved(it)
|
||||
|
||||
TestMessages.insertIncomingTextMessage(other = recipient, body = "Cool text message?!?!")
|
||||
TestMessages.insertIncomingImageMessage(other = recipient, attachmentCount = 1)
|
||||
TestMessages.insertIncomingImageMessage(other = recipient, attachmentCount = 2, body = "Album")
|
||||
TestMessages.insertIncomingImageMessage(other = recipient, body = "Test", attachmentCount = 1, failed = true)
|
||||
|
||||
SignalDatabase.messages.setAllMessagesRead()
|
||||
|
||||
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupConversationOpen() {
|
||||
TestUsers.setupSelf()
|
||||
TestUsers.setupTestRecipient().let {
|
||||
val recipient: Recipient = Recipient.resolved(it)
|
||||
val messagesToAdd = 1000
|
||||
val generator: TestMessages.TimestampGenerator = TestMessages.TimestampGenerator(System.currentTimeMillis() - (messagesToAdd * 2000L) - 60_000L)
|
||||
|
||||
for (i in 0 until messagesToAdd) {
|
||||
TestMessages.insertIncomingTextMessage(other = recipient, body = "Test message $i", timestamp = generator.nextTimestamp())
|
||||
TestMessages.insertOutgoingTextMessage(other = recipient, body = "Test message $i", timestamp = generator.nextTimestamp())
|
||||
}
|
||||
|
||||
val voiceMessageId = TestMessages.insertIncomingVoiceMessage(other = recipient, timestamp = generator.nextTimestamp())
|
||||
val mmsRecord = SignalDatabase.messages.getMessageRecord(voiceMessageId) as MediaMmsMessageRecord
|
||||
TestMessages.insertOutgoingImageMessage(other = recipient, body = "test", 2, generator.nextTimestamp())
|
||||
TestMessages.insertIncomingTextMessage(other = recipient, "reply to the test message", generator.nextTimestamp())
|
||||
TestMessages.insertIncomingQuoteTextMessage(other = recipient, quote = QuoteModel(mmsRecord.timestamp, recipient.id, "Fake voice message text", false, mmsRecord.slideDeck.asAttachments(), null, QuoteModel.Type.NORMAL, null), body = "Here is a cool quote", timestamp = generator.nextTimestamp())
|
||||
TestMessages.insertOutgoingTextMessage(other = recipient, body = "longaweorijoaijwerijoiajwer", timestamp = generator.nextTimestamp())
|
||||
|
||||
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package org.signal.benchmark
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
|
||||
class DummyAccountManagerFactory : AccountManagerFactory() {
|
||||
override fun createAuthenticated(context: Context, aci: ACI, pni: PNI, number: String, deviceId: Int, password: String): SignalServiceAccountManager {
|
||||
return DummyAccountManager(
|
||||
ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number),
|
||||
aci,
|
||||
pni,
|
||||
number,
|
||||
deviceId,
|
||||
password,
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
FeatureFlags.okHttpAutomaticRetry(),
|
||||
FeatureFlags.groupLimits().hardLimit
|
||||
)
|
||||
}
|
||||
|
||||
private class DummyAccountManager(configuration: SignalServiceConfiguration?, aci: ACI?, pni: PNI?, e164: String?, deviceId: Int, password: String?, signalAgent: String?, automaticNetworkRetry: Boolean, maxGroupSize: Int) : SignalServiceAccountManager(configuration, aci, pni, e164, deviceId, password, signalAgent, automaticNetworkRetry, maxGroupSize) {
|
||||
@Throws(IOException::class)
|
||||
override fun setGcmId(gcmRegistrationId: Optional<String>) {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun setPreKeys(serviceIdType: ServiceIdType, identityKey: IdentityKey, signedPreKey: SignedPreKeyRecord, oneTimePreKeys: List<PreKeyRecord>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package org.signal.benchmark.setup
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.TestDbUtils
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import java.util.Collections
|
||||
import java.util.Optional
|
||||
|
||||
object TestMessages {
|
||||
fun insertOutgoingTextMessage(other: Recipient, body: String, timestamp: Long = System.currentTimeMillis()) {
|
||||
insertOutgoingMessage(
|
||||
recipient = other,
|
||||
message = OutgoingMessage(
|
||||
recipient = other,
|
||||
body = body,
|
||||
timestamp = timestamp,
|
||||
isSecure = true
|
||||
),
|
||||
timestamp = timestamp
|
||||
)
|
||||
}
|
||||
|
||||
fun insertOutgoingImageMessage(other: Recipient, body: String? = null, attachmentCount: Int, timestamp: Long = System.currentTimeMillis()): Long {
|
||||
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
|
||||
imageAttachment()
|
||||
}
|
||||
val message = OutgoingMessage(
|
||||
recipient = other,
|
||||
body = body,
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
|
||||
timestamp = timestamp,
|
||||
isSecure = true
|
||||
)
|
||||
return insertOutgoingMediaMessage(recipient = other, message = message, timestamp = timestamp)
|
||||
}
|
||||
|
||||
private fun insertOutgoingMediaMessage(recipient: Recipient, message: OutgoingMessage, timestamp: Long): Long {
|
||||
val insert = insertOutgoingMessage(recipient, message = message, timestamp = timestamp)
|
||||
setMessageMediaTransfered(insert)
|
||||
|
||||
return insert
|
||||
}
|
||||
|
||||
private fun insertOutgoingMessage(recipient: Recipient, message: OutgoingMessage, timestamp: Long? = null): Long {
|
||||
val insert = SignalDatabase.messages.insertMessageOutbox(
|
||||
message,
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(recipient),
|
||||
false,
|
||||
null
|
||||
)
|
||||
if (timestamp != null) {
|
||||
TestDbUtils.setMessageReceived(insert, timestamp)
|
||||
}
|
||||
SignalDatabase.messages.markAsSent(insert, true)
|
||||
|
||||
return insert
|
||||
}
|
||||
fun insertIncomingTextMessage(other: Recipient, body: String, timestamp: Long? = null) {
|
||||
val message = IncomingMediaMessage(
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis()
|
||||
)
|
||||
|
||||
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get().messageId
|
||||
}
|
||||
fun insertIncomingQuoteTextMessage(other: Recipient, body: String, quote: QuoteModel, timestamp: Long?) {
|
||||
val message = IncomingMediaMessage(
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
quote = quote
|
||||
)
|
||||
insertIncomingMessage(other, message = message)
|
||||
}
|
||||
fun insertIncomingImageMessage(other: Recipient, body: String? = null, attachmentCount: Int, timestamp: Long? = null, failed: Boolean = false): Long {
|
||||
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
|
||||
imageAttachment()
|
||||
}
|
||||
val message = IncomingMediaMessage(
|
||||
from = other.id,
|
||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||
)
|
||||
return insertIncomingMediaMessage(recipient = other, message = message, failed = failed)
|
||||
}
|
||||
|
||||
fun insertIncomingVoiceMessage(other: Recipient, timestamp: Long? = null): Long {
|
||||
val message = IncomingMediaMessage(
|
||||
from = other.id,
|
||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
attachments = PointerAttachment.forPointers(Optional.of(Collections.singletonList(voiceAttachment()) as List<SignalServiceAttachment>))
|
||||
)
|
||||
return insertIncomingMediaMessage(recipient = other, message = message, failed = false)
|
||||
}
|
||||
|
||||
private fun insertIncomingMediaMessage(recipient: Recipient, message: IncomingMediaMessage, failed: Boolean = false): Long {
|
||||
val id = insertIncomingMessage(recipient = recipient, message = message)
|
||||
if (failed) {
|
||||
setMessageMediaFailed(id)
|
||||
} else {
|
||||
setMessageMediaTransfered(id)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
private fun insertIncomingMessage(recipient: Recipient, message: IncomingMediaMessage): Long {
|
||||
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(recipient)).get().messageId
|
||||
}
|
||||
|
||||
private fun setMessageMediaFailed(messageId: Long) {
|
||||
SignalDatabase.attachments.getAttachmentsForMessage(messageId).forEachIndexed { index, attachment ->
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, messageId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMessageMediaTransfered(messageId: Long) {
|
||||
SignalDatabase.attachments.getAttachmentsForMessage(messageId).forEachIndexed { _, attachment ->
|
||||
SignalDatabase.attachments.setTransferState(messageId, attachment.attachmentId, AttachmentTable.TRANSFER_PROGRESS_DONE)
|
||||
}
|
||||
}
|
||||
private fun imageAttachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
ReleaseChannel.CDN_NUMBER,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
"image/webp",
|
||||
null,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
1024,
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.of("/not-there.jpg"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
private fun voiceAttachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
ReleaseChannel.CDN_NUMBER,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
"audio/aac",
|
||||
null,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
1024,
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.of("/not-there.aac"),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
class TimestampGenerator(private var start: Long = System.currentTimeMillis()) {
|
||||
fun nextTimestamp(): Long {
|
||||
start += 500L
|
||||
|
||||
return start
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package org.signal.benchmark.setup
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import org.signal.benchmark.DummyAccountManagerFactory
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.util.UUID
|
||||
|
||||
object TestUsers {
|
||||
|
||||
private var generatedOthers: Int = 0
|
||||
|
||||
fun setupSelf(): Recipient {
|
||||
val application: Application = ApplicationDependencies.getApplication()
|
||||
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
|
||||
val masterSecret = MasterSecretUtil.generateMasterSecret(application, MasterSecretUtil.UNENCRYPTED_PASSPHRASE)
|
||||
MasterSecretUtil.generateAsymmetricMasterSecret(application, masterSecret)
|
||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||
|
||||
val registrationRepository = RegistrationRepository(application)
|
||||
val registrationData = RegistrationData(
|
||||
code = "123123",
|
||||
e164 = "+15555550101",
|
||||
password = Util.getSecret(18),
|
||||
registrationId = registrationRepository.registrationId,
|
||||
profileKey = registrationRepository.getProfileKey("+15555550101"),
|
||||
fcmToken = "fcm-token",
|
||||
pniRegistrationId = registrationRepository.pniRegistrationId,
|
||||
recoveryPassword = "asdfasdfasdfasdf"
|
||||
)
|
||||
val verifyResponse = VerifyResponse(VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false), null, null)
|
||||
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
|
||||
val response: ServiceResponse<VerifyResponse> = registrationRepository.registerAccount(
|
||||
registrationData,
|
||||
verifyResponse,
|
||||
false
|
||||
).blockingGet()
|
||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||
|
||||
SignalStore.kbsValues().optOut()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
return Recipient.self()
|
||||
}
|
||||
|
||||
fun setupTestRecipient(): RecipientId {
|
||||
return setupTestRecipients(1).first()
|
||||
}
|
||||
|
||||
fun setupTestRecipients(othersCount: Int): List<RecipientId> {
|
||||
val others = mutableListOf<RecipientId>()
|
||||
synchronized(this) {
|
||||
if (generatedOthers + othersCount !in 0 until 1000) {
|
||||
throw IllegalArgumentException("$othersCount must be between 0 and 1000")
|
||||
}
|
||||
|
||||
for (i in generatedOthers until generatedOthers + othersCount) {
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
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.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
||||
|
||||
others += recipientId
|
||||
}
|
||||
|
||||
generatedOthers += othersCount
|
||||
}
|
||||
|
||||
return others
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import org.signal.core.util.SqlUtil.buildArgs
|
||||
|
||||
object TestDbUtils {
|
||||
|
||||
fun setMessageReceived(messageId: Long, timestamp: Long) {
|
||||
val database: SQLiteDatabase = SignalDatabase.messages.databaseHelper.signalWritableDatabase
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MessageTable.DATE_RECEIVED, timestamp)
|
||||
val rowsUpdated = database.update(MessageTable.TABLE_NAME, contentValues, DatabaseTable.ID_WHERE, buildArgs(messageId))
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".CanaryApplicationContext"
|
||||
tools:replace="android:name" />
|
||||
|
||||
</manifest>
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import leakcanary.LeakCanary
|
||||
import shark.AndroidReferenceMatchers
|
||||
|
||||
class CanaryApplicationContext : ApplicationContext() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
|
||||
try {
|
||||
Class.forName("dalvik.system.CloseGuard")
|
||||
.getMethod("setEnabled", Boolean::class.javaPrimitiveType)
|
||||
.invoke(null, true)
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.service.media.MediaBrowserService\$ServiceBinder",
|
||||
fieldName = "this\$0"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "androidx.media.MediaBrowserServiceCompat\$MediaBrowserServiceImplApi26\$MediaBrowserServiceApi26",
|
||||
fieldName = "mBase"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.MediaBrowserCompat",
|
||||
fieldName = "mImpl"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||
fieldName = "mToken"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||
fieldName = "mImpl"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService",
|
||||
fieldName = "mApplication"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.service.GenericForegroundService\$LocalBinder",
|
||||
fieldName = "this\$0"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.contacts.ContactsSyncAdapter",
|
||||
fieldName = "mContext"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
16
app/src/flipper/AndroidManifest.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<application
|
||||
android:name=".FlipperApplicationContext"
|
||||
tools:replace="android:name">
|
||||
|
||||
<activity
|
||||
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||
android:exported="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter;
|
||||
|
||||
public class FlipperApplicationContext extends ApplicationContext {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
SoLoader.init(this, false);
|
||||
|
||||
FlipperClient client = AndroidFlipperClient.getInstance(this);
|
||||
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new DatabasesFlipperPlugin(new FlipperSqlCipherAdapter(this)));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
|
||||
client.start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
||||
|
||||
import net.sqlcipher.DatabaseUtils;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
|
||||
* and made to work with SqlCipher. Unfortunately I couldn't use it directly, nor subclass it.
|
||||
*/
|
||||
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
||||
|
||||
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
|
||||
|
||||
public FlipperSqlCipherAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Descriptor> getDatabases() {
|
||||
try {
|
||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||
databaseHelperField.setAccessible(true);
|
||||
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
|
||||
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, "Unable to use reflection to access raw database.", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTableNames(Descriptor descriptor) {
|
||||
SQLiteDatabase db = descriptor.getReadable();
|
||||
List<String> tableNames = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" })) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
tableNames.add(cursor.getString(0));
|
||||
}
|
||||
}
|
||||
|
||||
return tableNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseGetTableDataResponse getTableData(Descriptor descriptor, String table, String order, boolean reverse, int start, int count) {
|
||||
SQLiteDatabase db = descriptor.getReadable();
|
||||
|
||||
long total = DatabaseUtils.queryNumEntries(db, table);
|
||||
String orderBy = order != null ? order + (reverse ? " DESC" : " ASC") : null;
|
||||
String limitBy = start + ", " + count;
|
||||
|
||||
try (Cursor cursor = db.query(table, null, null, null, null, null, orderBy, limitBy)) {
|
||||
String[] columnNames = cursor.getColumnNames();
|
||||
List<List<Object>> rows = cursorToList(cursor);
|
||||
|
||||
return new DatabaseGetTableDataResponse(Arrays.asList(columnNames), rows, start, rows.size(), total);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseGetTableStructureResponse getTableStructure(Descriptor descriptor, String table) {
|
||||
SQLiteDatabase db = descriptor.getReadable();
|
||||
|
||||
Map<String, String> foreignKeyValues = new HashMap<>();
|
||||
|
||||
try(Cursor cursor = db.rawQuery("PRAGMA foreign_key_list(" + table + ")", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String from = cursor.getString(cursor.getColumnIndex("from"));
|
||||
String to = cursor.getString(cursor.getColumnIndex("to"));
|
||||
String tableName = cursor.getString(cursor.getColumnIndex("table")) + "(" + to + ")";
|
||||
|
||||
foreignKeyValues.put(from, tableName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<String> structureColumns = Arrays.asList("column_name", "data_type", "nullable", "default", "primary_key", "foreign_key");
|
||||
List<List<Object>> structureValues = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String columnName = cursor.getString(cursor.getColumnIndex("name"));
|
||||
String foreignKey = foreignKeyValues.containsKey(columnName) ? foreignKeyValues.get(columnName) : null;
|
||||
|
||||
structureValues.add(Arrays.asList(columnName,
|
||||
cursor.getString(cursor.getColumnIndex("type")),
|
||||
cursor.getInt(cursor.getColumnIndex("notnull")) == 0,
|
||||
getObjectFromColumnIndex(cursor, cursor.getColumnIndex("dflt_value")),
|
||||
cursor.getInt(cursor.getColumnIndex("pk")) == 1,
|
||||
foreignKey));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<String> indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name");
|
||||
List<List<Object>> indexesValues = new ArrayList<>();
|
||||
|
||||
try (Cursor indexesCursor = db.rawQuery("PRAGMA index_list(" + table + ")", null)) {
|
||||
List<String> indexedColumnNames = new ArrayList<>();
|
||||
String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name"));
|
||||
|
||||
try(Cursor indexInfoCursor = db.rawQuery("PRAGMA index_info(" + indexName + ")", null)) {
|
||||
while (indexInfoCursor.moveToNext()) {
|
||||
indexedColumnNames.add(indexInfoCursor.getString(indexInfoCursor.getColumnIndex("name")));
|
||||
}
|
||||
}
|
||||
|
||||
indexesValues.add(Arrays.asList(indexName,
|
||||
indexesCursor.getInt(indexesCursor.getColumnIndex("unique")) == 1,
|
||||
TextUtils.join(",", indexedColumnNames)));
|
||||
|
||||
}
|
||||
|
||||
return new DatabaseGetTableStructureResponse(structureColumns, structureValues, indexesColumns, indexesValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseGetTableInfoResponse getTableInfo(Descriptor databaseDescriptor, String table) {
|
||||
SQLiteDatabase db = databaseDescriptor.getReadable();
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT sql FROM sqlite_master WHERE name = ?", new String[] { table })) {
|
||||
cursor.moveToFirst();
|
||||
return new DatabaseGetTableInfoResponse(cursor.getString(cursor.getColumnIndex("sql")));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseExecuteSqlResponse executeSQL(Descriptor descriptor, String query) {
|
||||
SQLiteDatabase db = descriptor.getWritable();
|
||||
|
||||
String firstWordUpperCase = getFirstWord(query).toUpperCase();
|
||||
|
||||
switch (firstWordUpperCase) {
|
||||
case "UPDATE":
|
||||
case "DELETE":
|
||||
return executeUpdateDelete(db, query);
|
||||
case "INSERT":
|
||||
return executeInsert(db, query);
|
||||
case "SELECT":
|
||||
case "PRAGMA":
|
||||
case "EXPLAIN":
|
||||
return executeSelect(db, query);
|
||||
default:
|
||||
return executeRawQuery(db, query);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getFirstWord(String s) {
|
||||
s = s.trim();
|
||||
int firstSpace = s.indexOf(' ');
|
||||
return firstSpace >= 0 ? s.substring(0, firstSpace) : s;
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeUpdateDelete(SQLiteDatabase database, String query) {
|
||||
SQLiteStatement statement = database.compileStatement(query);
|
||||
int count = statement.executeUpdateDelete();
|
||||
|
||||
return DatabaseExecuteSqlResponse.successfulUpdateDelete(count);
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeInsert(SQLiteDatabase database, String query) {
|
||||
SQLiteStatement statement = database.compileStatement(query);
|
||||
long insertedId = statement.executeInsert();
|
||||
|
||||
return DatabaseExecuteSqlResponse.successfulInsert(insertedId);
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeSelect(SQLiteDatabase database, String query) {
|
||||
try (Cursor cursor = database.rawQuery(query, null)) {
|
||||
String[] columnNames = cursor.getColumnNames();
|
||||
List<List<Object>> rows = cursorToList(cursor);
|
||||
|
||||
return DatabaseExecuteSqlResponse.successfulSelect(Arrays.asList(columnNames), rows);
|
||||
}
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeRawQuery(SQLiteDatabase database, String query) {
|
||||
database.execSQL(query);
|
||||
return DatabaseExecuteSqlResponse.successfulRawQuery();
|
||||
}
|
||||
|
||||
private static @NonNull List<List<Object>> cursorToList(Cursor cursor) {
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
int numColumns = cursor.getColumnCount();
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
List<Object> values = new ArrayList<>(numColumns);
|
||||
|
||||
for (int column = 0; column < numColumns; column++) {
|
||||
values.add(getObjectFromColumnIndex(cursor, column));
|
||||
}
|
||||
|
||||
rows.add(values);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static @Nullable Object getObjectFromColumnIndex(Cursor cursor, int column) {
|
||||
switch (cursor.getType(column)) {
|
||||
case Cursor.FIELD_TYPE_NULL:
|
||||
return null;
|
||||
case Cursor.FIELD_TYPE_INTEGER:
|
||||
return cursor.getLong(column);
|
||||
case Cursor.FIELD_TYPE_FLOAT:
|
||||
return cursor.getDouble(column);
|
||||
case Cursor.FIELD_TYPE_BLOB:
|
||||
return cursor.getBlob(column);
|
||||
case Cursor.FIELD_TYPE_STRING:
|
||||
default:
|
||||
return cursor.getString(column);
|
||||
}
|
||||
}
|
||||
|
||||
static class Descriptor implements DatabaseDescriptor {
|
||||
private final SQLCipherOpenHelper sqlCipherOpenHelper;
|
||||
|
||||
Descriptor(@NonNull SQLCipherOpenHelper sqlCipherOpenHelper) {
|
||||
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return sqlCipherOpenHelper.getDatabaseName();
|
||||
}
|
||||
|
||||
public @NonNull SQLiteDatabase getReadable() {
|
||||
return sqlCipherOpenHelper.getReadableDatabase().getSqlCipherDatabase();
|
||||
}
|
||||
|
||||
public @NonNull SQLiteDatabase getWritable() {
|
||||
return sqlCipherOpenHelper.getWritableDatabase().getSqlCipherDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:usesCleartextTraffic"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
</manifest>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/signal_accent_green"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<resources>
|
||||
<string name="app_name">Signal (Instrumentation)</string>
|
||||
</resources>
|
||||
5
app/src/internal/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/core_red_shade"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle,androidx.camera.view" />
|
||||
|
||||
@@ -21,6 +22,7 @@
|
||||
<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.WRITE_PROFILE"/>
|
||||
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
@@ -62,6 +64,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
|
||||
<!-- So we can add a TextSecure 'Account' -->
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
@@ -89,31 +92,18 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<application android:name=".ApplicationContext"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:resizeableActivity="true"
|
||||
android:fullBackupOnly="false"
|
||||
android:allowBackup="true"
|
||||
android:backupAgent=".absbackup.SignalBackupAgent"
|
||||
tools:replace="android:allowBackup"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:largeHeap="true">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.wallet.api.enabled"
|
||||
android:value="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${mapsKey}"/>
|
||||
|
||||
<meta-data android:name="android.supports_size_changes"
|
||||
android:value="true" />
|
||||
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
|
||||
|
||||
<meta-data android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version" />
|
||||
@@ -128,15 +118,16 @@
|
||||
<activity android:name=".WebRtcCallActivity"
|
||||
android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
|
||||
android:excludeFromRecents="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:taskAffinity=".calling"
|
||||
android:resizeableActivity="true"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:screenOrientation="portrait"
|
||||
android:noHistory="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -156,36 +147,24 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DeviceProvisioningActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="true">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tsdevice"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="linkdevice"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".preferences.MmsPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity android:name=".sharing.v2.ShareActivity"
|
||||
<activity android:name=".sharing.ShareActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
@@ -209,25 +188,24 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
android:value=".service.DirectShareService" />
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".stickers.StickerPackPreviewActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" android:exported="true" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="addstickers" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -247,22 +225,7 @@
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name=".deeplinks.DeepLinkEntryActivity"
|
||||
android:exported="true"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/Signal.Transparent">
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -279,36 +242,12 @@
|
||||
android:host="signal.group"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https"
|
||||
android:host="signal.tube" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="signal.tube" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https"
|
||||
android:host="signal.me" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="signal.me" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".conversation.v2.ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
</activity>
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name=".conversation.ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
@@ -325,6 +264,8 @@
|
||||
android:allowEmbedded="true"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity android:name=".longmessage.LongMessageActivity" />
|
||||
|
||||
<activity android:name=".conversation.ConversationPopupActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:launchMode="singleTask"
|
||||
@@ -333,17 +274,30 @@
|
||||
android:theme="@style/TextSecure.LightTheme.Popup"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".migrations.ApplicationMigrationActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -364,11 +318,6 @@
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".calls.new.NewCallActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PushContactSelectionActivity"
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -379,114 +328,31 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.v2.MediaSelectionActivity"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".conversation.mutiselect.forward.MultiselectForwardActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".mediasend.v2.stories.StoriesMultiselectForwardActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
<activity android:name=".mediasend.MediaSendActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".verify.VerifyIdentityActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
<activity android:name=".VerifyIdentityActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".components.settings.app.AppSettingsActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<activity android:name=".ApplicationPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.NOTIFICATION_PREFERENCES" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".stories.my.MyStoriesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.settings.StorySettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.viewer.StoryViewerActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar.StoryViewer"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:parentActivityName=".MainActivity">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.ConversationSettings"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".components.settings.conversation.CallInfoActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".badges.gifts.flow.GiftFlowActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".wallpaper.ChatWallpaperActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".wallpaper.ChatWallpaperPreviewActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".devicetransfer.olddevice.OldDeviceTransferActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".devicetransfer.olddevice.OldDeviceExitActivity"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".registration.RegistrationNavigationActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".revealable.ViewOnceMessageActivity"
|
||||
@@ -503,7 +369,6 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DeviceActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:label="@string/AndroidManifest__linked_devices"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -511,12 +376,10 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediapreview.MediaPreviewV2Activity"
|
||||
<activity android:name=".MediaPreviewActivity"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="false"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".AvatarPreviewActivity"
|
||||
@@ -524,13 +387,6 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity
|
||||
android:name=".avatar.photo.PhotoEditorActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -548,11 +404,10 @@
|
||||
android:finishOnTaskLaunch="true" />
|
||||
|
||||
<activity android:name=".PlayServicesProblemActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/TextSecure.DialogActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".SmsSendtoActivity" android:exported="true">
|
||||
<activity android:name=".SmsSendtoActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -571,9 +426,7 @@
|
||||
</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">
|
||||
@@ -587,34 +440,21 @@
|
||||
</activity>
|
||||
|
||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".blocked.BlockedUsersActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:theme="@style/TextSecure.DarkTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".profiles.edit.EditProfileActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
||||
<activity android:name=".profiles.username.AddAUsernameActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
||||
<activity android:name=".profiles.manage.ManageProfileActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
||||
<activity android:name=".payments.preferences.PaymentsActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".lock.v2.CreateKbsPinActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
@@ -625,6 +465,12 @@
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ClearAvatarPromptActivity"
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert"
|
||||
android:icon="@drawable/clear_profile_avatar"
|
||||
android:label="@string/AndroidManifest_remove_photo"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert" />
|
||||
|
||||
@@ -658,6 +504,7 @@
|
||||
|
||||
<activity android:name=".MainActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".pin.PinRestoreActivity"
|
||||
@@ -684,59 +531,18 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<activity android:name=".megaphone.SmsExportMegaphoneActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<activity android:name=".ratelimit.RecaptchaProofActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" />
|
||||
|
||||
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar" />
|
||||
|
||||
<activity android:name=".wallpaper.crop.WallpaperCropActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Signal.WallpaperCropper" />
|
||||
|
||||
<activity android:name=".reactions.edit.EditReactionsActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".exporter.flow.SmsExportActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<service android:enabled="true" android:name=".exporter.SignalSmsExportService" android:foregroundServiceType="dataSync" />
|
||||
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/>
|
||||
<service android:enabled="true" android:name=".service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$BackgroundService"/>
|
||||
<service android:name=".service.webrtc.AndroidCallConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".components.voice.VoiceNotePlaybackService" android:exported="true">
|
||||
<service android:name=".components.voice.VoiceNotePlaybackService">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
@@ -770,13 +576,18 @@
|
||||
<meta-data android:name="android.provider.CONTACTS_STRUCTURE" android:resource="@xml/contactsformat" />
|
||||
</service>
|
||||
|
||||
<service android:name=".service.DirectShareService"
|
||||
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.chooser.ChooserTargetService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".service.GenericForegroundService"/>
|
||||
|
||||
<service android:name=".gcm.FcmFetchBackgroundService" />
|
||||
<service android:name=".gcm.FcmFetchService" />
|
||||
|
||||
<service android:name=".gcm.FcmFetchForegroundService" />
|
||||
|
||||
<service android:name=".gcm.FcmReceiveService" android:exported="true">
|
||||
<service android:name=".gcm.FcmReceiveService">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
@@ -831,20 +642,26 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.ExpirationListener" />
|
||||
<receiver android:name=".notifications.AndroidAutoHeardReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.notifications.ANDROID_AUTO_HEARD"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.ExpiringStoriesManager$ExpireStoriesAlarm" />
|
||||
<receiver android:name=".notifications.AndroidAutoReplyReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.notifications.ANDROID_AUTO_REPLY"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.ExpirationListener" />
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.ScheduledMessageManager$ScheduledMessagesAlarm" />
|
||||
|
||||
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
|
||||
|
||||
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
|
||||
|
||||
<receiver android:name=".payments.backup.phrase.ClearClipboardAlarmReceiver" />
|
||||
|
||||
<provider android:name=".providers.PartProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
@@ -869,53 +686,64 @@
|
||||
|
||||
</provider>
|
||||
|
||||
<receiver android:name=".service.BootReceiver" android:exported="false">
|
||||
<provider android:name=".database.DatabaseContentProviders$Conversation"
|
||||
android:authorities="${applicationId}.database.conversation"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$ConversationList"
|
||||
android:authorities="${applicationId}.database.conversationlist"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Attachment"
|
||||
android:authorities="${applicationId}.database.attachment"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Sticker"
|
||||
android:authorities="${applicationId}.database.sticker"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$StickerPack"
|
||||
android:authorities="${applicationId}.database.stickerpack"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name=".service.BootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="org.thoughtcrime.securesms.RESTART"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.DirectoryRefreshListener" android:exported="false">
|
||||
<receiver android:name=".service.DirectoryRefreshListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.RotateSignedPreKeyListener" android:exported="false">
|
||||
<receiver android:name=".service.RotateSignedPreKeyListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.RotateSenderCertificateListener" android:exported="false">
|
||||
<receiver android:name=".service.RotateSenderCertificateListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".messageprocessingalarm.MessageProcessReceiver" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="org.thoughtcrime.securesms.action.PROCESS_MESSAGES" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.LocalBackupListener" android:exported="false">
|
||||
<receiver android:name=".service.LocalBackupListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
|
||||
|
||||
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
|
||||
<receiver android:name=".service.PersistentConnectionBootListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.LocaleChangedReceiver" android:exported="false">
|
||||
<receiver android:name=".notifications.LocaleChangedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED"/>
|
||||
</intent-filter>
|
||||
@@ -923,7 +751,7 @@
|
||||
|
||||
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver"/>
|
||||
|
||||
<receiver android:name=".notifications.DeleteNotificationReceiver" android:exported="false">
|
||||
<receiver android:name=".notifications.DeleteNotificationReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.DELETE_NOTIFICATION"/>
|
||||
</intent-filter>
|
||||
@@ -951,19 +779,16 @@
|
||||
|
||||
<service
|
||||
android:name=".jobmanager.KeepAliveService"
|
||||
android:enabled="@bool/enable_alarm_manager"
|
||||
android:exported="false"/>
|
||||
android:enabled="@bool/enable_alarm_manager" />
|
||||
|
||||
<receiver
|
||||
android:name=".jobmanager.AlarmManagerScheduler$RetryReceiver"
|
||||
android:enabled="@bool/enable_alarm_manager"
|
||||
android:exported="false"/>
|
||||
android:enabled="@bool/enable_alarm_manager" />
|
||||
|
||||
<!-- Probably don't need this one -->
|
||||
<receiver
|
||||
android:name=".jobmanager.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 664 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 631 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 653 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 652 KiB |