Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b08ebe9cae | ||
|
|
6be5319a1f | ||
|
|
74eb52b126 | ||
|
|
3825eed6ad | ||
|
|
20996c985b | ||
|
|
e5e9a7108e | ||
|
|
51dfee9cd3 |
@@ -1,4 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*.kt]
|
||||
indent_size = 2
|
||||
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
|
||||
24
.github/workflows/android.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'master'
|
||||
- '4.**'
|
||||
- '5.**'
|
||||
|
||||
@@ -14,26 +14,18 @@ jobs:
|
||||
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
|
||||
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: Remove Android 31 (S)
|
||||
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-31"
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew qa
|
||||
|
||||
- name: Archive reports for failed build
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: reports
|
||||
path: '*/build/reports'
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
||||
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
|
||||
|
||||
226
.idea/codeStyles/Project.xml
generated
@@ -1,226 +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>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
</JetCodeStyleSettings>
|
||||
<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()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ Signal is a messaging app for simple private communication with friends.
|
||||
|
||||
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>
|
||||
|
||||
@@ -28,7 +28,7 @@ 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.
|
||||
|
||||
@@ -59,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])
|
||||
|
||||
576
app/build.gradle
@@ -1,45 +1,54 @@
|
||||
import org.signal.signing.ApkSignerUtil
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'androidx.navigation.safeargs'
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
apply plugin: 'witness'
|
||||
apply from: 'translations.gradle'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'app.cash.exhaustive'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply from: 'static-ips.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 {
|
||||
url "https://www.jitpack.io"
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven {
|
||||
url "https://dl.cloudsmith.io/qxAgwaeEE1vN8aLU/mobilecoin/mobilecoin/maven/"
|
||||
}
|
||||
jcenter {
|
||||
maven { // textdrawable
|
||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
||||
content {
|
||||
includeVersion "mobi.upod", "time-duration-picker", "1.1.3"
|
||||
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
|
||||
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
|
||||
includeVersion "com.codewaves.stickyheadergrid", "stickyheadergrid", "0.9.4"
|
||||
includeVersion "com.google.android", "flexbox", "0.3.0"
|
||||
includeGroupByRegex "com\\.amulyakhare.*"
|
||||
}
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.18.0'
|
||||
artifact = 'com.google.protobuf:protoc:3.10.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
@@ -52,39 +61,18 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
|
||||
version = "0.43.2"
|
||||
}
|
||||
def canonicalVersionCode = 759
|
||||
def canonicalVersionName = "5.0.10"
|
||||
|
||||
def canonicalVersionCode = 1082
|
||||
def canonicalVersionName = "5.42.4"
|
||||
|
||||
def postFixSize = 100
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
'armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
'armeabi-v7a' : 8,
|
||||
'arm64-v8a' : 9,
|
||||
'x86' : 1,
|
||||
'x86_64' : 2]
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
def selectableVariants = [
|
||||
'nightlyProdSpinner',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'playProdDebug',
|
||||
'playProdSpinner',
|
||||
'playProdPerf',
|
||||
'playProdRelease',
|
||||
'playStagingDebug',
|
||||
'playStagingSpinner',
|
||||
'playStagingPerf',
|
||||
'playStagingRelease',
|
||||
'websiteProdSpinner',
|
||||
'websiteProdRelease',
|
||||
]
|
||||
|
||||
android {
|
||||
buildToolsVersion BUILD_TOOL_VERSION
|
||||
compileSdkVersion COMPILE_SDK
|
||||
@@ -92,11 +80,6 @@ android {
|
||||
flavorDimensions 'distribution', 'environment'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = ["-Xallow-result-return-type"]
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
javaMaxHeapSize "4g"
|
||||
}
|
||||
@@ -112,50 +95,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
disable "LintError"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
|
||||
androidTest {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
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'
|
||||
exclude 'libsignal_jni.dylib'
|
||||
exclude 'signal_jni.dll'
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
@@ -169,49 +108,27 @@ android {
|
||||
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.staging.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_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\"}"
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.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", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "String", "CDSI_MRENCLAVE", "\"42e36b74794abe612d698308b148ff8a7dc5fdc6ad28d99bc5024ed6ece18dfe\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
|
||||
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
|
||||
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+ikU5gBXsRmoF94GXQ==\""
|
||||
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 "int", "TRACE_EVENT_MAX", "3500"
|
||||
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
@@ -229,7 +146,25 @@ android {
|
||||
}
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -238,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',
|
||||
@@ -247,6 +182,7 @@ 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',
|
||||
@@ -259,28 +195,16 @@ android {
|
||||
'proguard/proguard.cfg'
|
||||
testProguardFiles 'proguard/proguard-automation.pro',
|
||||
'proguard/proguard.cfg'
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||
}
|
||||
spinner {
|
||||
flipper {
|
||||
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\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,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 {
|
||||
@@ -299,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 {
|
||||
@@ -325,230 +243,240 @@ 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_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
|
||||
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
|
||||
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+UwyIlIT9L7KLS19Aw8r4sPrXQ==\""
|
||||
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=\""
|
||||
}
|
||||
}
|
||||
|
||||
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.core.ktx
|
||||
implementation libs.androidx.fragment.ktx
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
coreLibraryDesugaring libs.android.tools.desugar
|
||||
|
||||
implementation (libs.androidx.appcompat) {
|
||||
version {
|
||||
strictly '1.2.0'
|
||||
}
|
||||
implementation ('androidx.appcompat:appcompat:1.2.0') {
|
||||
force = true
|
||||
}
|
||||
implementation libs.androidx.window
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.material.material
|
||||
implementation libs.androidx.legacy.support
|
||||
implementation libs.androidx.cardview
|
||||
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.extensions
|
||||
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 '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 '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.photoview
|
||||
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.time.duration.picker
|
||||
implementation libs.google.zxing.core
|
||||
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.numberpickerview) {
|
||||
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation (libs.android.tooltips) {
|
||||
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation (libs.android.smsmms) {
|
||||
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 (libs.colorpicker) {
|
||||
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.stickyheadergrid
|
||||
|
||||
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 'dnsjava:dnsjava:2.1.9'
|
||||
|
||||
spinnerImplementation project(":spinner")
|
||||
spinnerImplementation libs.square.leakcanary
|
||||
flipperImplementation 'com.facebook.flipper:flipper:0.32.2'
|
||||
flipperImplementation 'com.facebook.soloader:soloader:0.8.2'
|
||||
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
testImplementation testLibs.mockito.core
|
||||
testImplementation testLibs.mockito.kotlin
|
||||
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.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(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
|
||||
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
|
||||
'pkcs11.config',
|
||||
'PKCS11',
|
||||
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
|
||||
outputFile.getAbsolutePath())
|
||||
|
||||
testImplementation testLibs.espresso.core
|
||||
inputFile.delete()
|
||||
outputFile
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.kotlin.stdlib.jdk8
|
||||
implementation libs.kotlin.reflect
|
||||
implementation libs.jackson.module.kotlin
|
||||
task signProductionPlayRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
implementation libs.rxdogtag
|
||||
task signProductionInternalRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
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 ->
|
||||
def result = exec {
|
||||
executable = 'git'
|
||||
@@ -560,40 +488,6 @@ def getLastCommitTimestamp() {
|
||||
}
|
||||
}
|
||||
|
||||
def getGitHash() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return "abcd1234"
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
def getCurrentGitTag() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return ''
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -614,9 +508,3 @@ def loadKeystoreProperties(filename) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
def getDateSuffix() {
|
||||
def date = new Date()
|
||||
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
|
||||
return formattedDate
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
<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" />
|
||||
|
||||
@@ -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.conversation
|
||||
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
/**
|
||||
* Android test to help show SNC dialog quickly with custom data to make sure it displays properly.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SafetyNumberChangeDialogPreviewer {
|
||||
|
||||
@get:Rule val harness = SignalActivityRule()
|
||||
|
||||
@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, IdentityDatabase.VerifiedStatus.VERIFIED)
|
||||
harness.changeIdentityKey(other)
|
||||
|
||||
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
|
||||
scenario.onActivity {
|
||||
SafetyNumberChangeDialog.show(it.supportFragmentManager, other.id)
|
||||
}
|
||||
|
||||
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
|
||||
// ThreadUtil.sleep(15000)
|
||||
}
|
||||
}
|
||||
@@ -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 DistributionListDatabaseTest {
|
||||
|
||||
private lateinit var distributionDatabase: DistributionListDatabase
|
||||
|
||||
@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,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 MmsDatabaseTest_gifts {
|
||||
private lateinit var mms: MmsDatabase
|
||||
|
||||
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.mms
|
||||
|
||||
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,241 +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.thoughtcrime.securesms.database.model.DistributionListId
|
||||
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
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MmsDatabaseTest_stories {
|
||||
|
||||
private lateinit var mms: MmsDatabase
|
||||
|
||||
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>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mms = SignalDatabase.mms
|
||||
|
||||
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())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectAnEmptyList() {
|
||||
// WHEN
|
||||
val result = mms.orderedStoryRecipientsAndIds
|
||||
|
||||
// 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.orderedStoryRecipientsAndIds
|
||||
|
||||
// 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.mms.getMessageRecord(messageId)
|
||||
assertFalse(messageBeforeMark.incomingStoryViewedAtTimestamp > 0)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.mms.setIncomingMessageViewed(messageId)
|
||||
|
||||
// THEN
|
||||
val messageAfterMark = SignalDatabase.mms.getMessageRecord(messageId)
|
||||
assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0)
|
||||
}
|
||||
|
||||
@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.mms.setIncomingMessageViewed(it)
|
||||
Thread.sleep(5)
|
||||
}
|
||||
|
||||
// WHEN
|
||||
val result = SignalDatabase.mms.orderedStoryRecipientsAndIds
|
||||
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.mms.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.mms.orderedStoryRecipientsAndIds
|
||||
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,
|
||||
threadId = -1L
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.isOutgoingStoryAlreadyInDatabase(myStory.id, 200)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
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.OutgoingMediaMessage
|
||||
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 = ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
threadId: Long = 1,
|
||||
storyType: StoryType = StoryType.NONE,
|
||||
giftBadge: GiftBadge? = null
|
||||
): Long {
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
body,
|
||||
emptyList(),
|
||||
sentTimeMillis,
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
giftBadge
|
||||
)
|
||||
|
||||
return insert(
|
||||
message = message,
|
||||
threadId = threadId
|
||||
)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: OutgoingMediaMessage,
|
||||
threadId: Long
|
||||
): Long {
|
||||
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: IncomingMediaMessage,
|
||||
threadId: Long
|
||||
): Optional<MessageDatabase.InsertResult> {
|
||||
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId)
|
||||
}
|
||||
}
|
||||
@@ -1,786 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.MatcherAssert
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.Assert
|
||||
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.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.state.SessionRecord
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
|
||||
import org.thoughtcrime.securesms.keyvalue.AccountValues
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||
import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
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.util.UuidUtil
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientDatabaseTest_getAndPossiblyMerge {
|
||||
|
||||
private lateinit var recipientDatabase: RecipientDatabase
|
||||
private lateinit var identityDatabase: IdentityDatabase
|
||||
private lateinit var groupReceiptDatabase: GroupReceiptDatabase
|
||||
private lateinit var groupDatabase: GroupDatabase
|
||||
private lateinit var threadDatabase: ThreadDatabase
|
||||
private lateinit var smsDatabase: MessageDatabase
|
||||
private lateinit var mmsDatabase: MessageDatabase
|
||||
private lateinit var sessionDatabase: SessionDatabase
|
||||
private lateinit var mentionDatabase: MentionDatabase
|
||||
private lateinit var reactionDatabase: ReactionDatabase
|
||||
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
|
||||
private lateinit var distributionListDatabase: DistributionListDatabase
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
recipientDatabase = SignalDatabase.recipients
|
||||
recipientDatabase = SignalDatabase.recipients
|
||||
identityDatabase = SignalDatabase.identities
|
||||
groupReceiptDatabase = SignalDatabase.groupReceipts
|
||||
groupDatabase = SignalDatabase.groups
|
||||
threadDatabase = SignalDatabase.threads
|
||||
smsDatabase = SignalDatabase.sms
|
||||
mmsDatabase = SignalDatabase.mms
|
||||
sessionDatabase = SignalDatabase.sessions
|
||||
mentionDatabase = SignalDatabase.mentions
|
||||
reactionDatabase = SignalDatabase.reactions
|
||||
notificationProfileDatabase = SignalDatabase.notificationProfiles
|
||||
distributionListDatabase = SignalDatabase.distributionLists
|
||||
|
||||
ensureDbEmpty()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// If both the ACI and E164 map to no one
|
||||
// ==============================================================
|
||||
|
||||
/** If all you have is an ACI, you can just store that, regardless of trust level. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_highTrust() {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireServiceId())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
/** If all you have is an ACI, you can just store that, regardless of trust level. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_lowTrust() {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireServiceId())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
/** If all you have is an E164, you can just store that, regardless of trust level. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_highTrust() {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
assertFalse(recipient.hasServiceId())
|
||||
}
|
||||
|
||||
/** If all you have is an E164, you can just store that, regardless of trust level. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_lowTrust() {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, false)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
assertFalse(recipient.hasServiceId())
|
||||
}
|
||||
|
||||
/** With high trust, you can associate an ACI-e164 pair. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_highTrust() {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireServiceId())
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
}
|
||||
|
||||
/** With low trust, you cannot associate an ACI-e164 pair, and therefore can only store the ACI. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_lowTrust() {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireServiceId())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// If the ACI maps to an existing user, but the E164 doesn't
|
||||
// ==============================================================
|
||||
|
||||
/** With high trust, you can associate an e164 with an existing ACI. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_highTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** With low trust, you cannot associate an ACI-e164 pair, and therefore cannot store the e164. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_lowTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
}
|
||||
|
||||
/** Basically the ‘change number’ case. High trust lets you update the existing user. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_highTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** Low trust means you can’t update the underlying data, but you also don’t need to create any new rows. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_lowTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, false)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// If the E164 maps to an existing user, but the ACI doesn't
|
||||
// ==============================================================
|
||||
|
||||
/** With high trust, you can associate an e164 with an existing ACI. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_highTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** With low trust, you cannot associate an ACI-e164 pair, and therefore need to create a new person with just the ACI. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_lowTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(E164_A, existingRecipient.requireE164())
|
||||
assertFalse(existingRecipient.hasServiceId())
|
||||
}
|
||||
|
||||
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. But high trust lets us take the e164 from the current holder. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_highTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
recipientDatabase.setPni(existingId, PNI_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||
recipientDatabase.setPni(retrievedId, PNI_A)
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireServiceId())
|
||||
assertFalse(existingRecipient.hasE164())
|
||||
}
|
||||
|
||||
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. And low trust means we can’t take the e164. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_lowTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, false)
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireServiceId())
|
||||
assertEquals(E164_A, existingRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** We never want to remove the e164 of our own contact entry. So basically treat this as a low-trust case, and leave the e164 alone. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser_highTrust() {
|
||||
val dataSet = KeyValueDataSet().apply {
|
||||
putString(AccountValues.KEY_E164, E164_A)
|
||||
putString(AccountValues.KEY_ACI, ACI_A.toString())
|
||||
}
|
||||
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
|
||||
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireServiceId())
|
||||
assertEquals(E164_A, existingRecipient.requireE164())
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// If both the ACI and E164 map to an existing user
|
||||
// ==============================================================
|
||||
|
||||
/** Regardless of trust, if your ACI and e164 match, you’re good. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_highTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
|
||||
val changeNumberListener = ChangeNumberListener()
|
||||
changeNumberListener.enqueue()
|
||||
|
||||
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
assertEquals(retrievedId, existingE164Recipient.id)
|
||||
|
||||
changeNumberListener.waitForJobManager()
|
||||
assertFalse(changeNumberListener.numberChangeWasEnqueued)
|
||||
}
|
||||
|
||||
/** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust], but with a number change. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust_changedNumber() {
|
||||
val changeNumberListener = ChangeNumberListener()
|
||||
changeNumberListener.enqueue()
|
||||
|
||||
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
assertEquals(retrievedId, existingE164Recipient.id)
|
||||
|
||||
changeNumberListener.waitForJobManager()
|
||||
assert(changeNumberListener.numberChangeWasEnqueued)
|
||||
}
|
||||
|
||||
/** Low trust means you can’t merge. If you’re retrieving a user from the table with this data, prefer the ACI one. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_lowTrust() {
|
||||
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
assertEquals(E164_A, existingE164Recipient.requireE164())
|
||||
assertFalse(existingE164Recipient.hasServiceId())
|
||||
}
|
||||
|
||||
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() {
|
||||
val changeNumberListener = ChangeNumberListener()
|
||||
changeNumberListener.enqueue()
|
||||
|
||||
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||
assertEquals(ACI_B, existingRecipient2.requireServiceId())
|
||||
assertFalse(existingRecipient2.hasE164())
|
||||
|
||||
assert(changeNumberListener.numberChangeWasEnqueued)
|
||||
}
|
||||
|
||||
/** Another low trust case. No new rules here, just a more complex scenario to show how different rules interact. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_lowTrust() {
|
||||
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
|
||||
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||
assertEquals(ACI_B, existingRecipient2.requireServiceId())
|
||||
assertEquals(E164_A, existingRecipient2.requireE164())
|
||||
}
|
||||
|
||||
/**
|
||||
* Another high trust case that results in a merge. Nothing strictly new here, but this case is called out because it’s a merge but *also* an E164 change,
|
||||
* which clients may need to know for UX purposes.
|
||||
*/
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange_highTrust() {
|
||||
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
|
||||
|
||||
val recipientWithId2 = Recipient.resolved(existingId2)
|
||||
assertEquals(retrievedId, recipientWithId2.id)
|
||||
}
|
||||
|
||||
/** We never want to remove the e164 of our own contact entry. So basically treat this as a low-trust case, and leave the e164 alone. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_e164BelongsToLocalUser() {
|
||||
val dataSet = KeyValueDataSet().apply {
|
||||
putString(AccountValues.KEY_E164, E164_A)
|
||||
putString(AccountValues.KEY_ACI, ACI_B.toString())
|
||||
}
|
||||
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
|
||||
|
||||
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId2, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val recipientWithId1 = Recipient.resolved(existingId1)
|
||||
assertEquals(ACI_B, recipientWithId1.requireServiceId())
|
||||
assertEquals(E164_A, recipientWithId1.requireE164())
|
||||
}
|
||||
|
||||
/** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfFalse() {
|
||||
val dataSet = KeyValueDataSet().apply {
|
||||
putString(AccountValues.KEY_E164, E164_A)
|
||||
putString(AccountValues.KEY_ACI, ACI_A.toString())
|
||||
}
|
||||
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
|
||||
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = false)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfTrue() {
|
||||
val dataSet = KeyValueDataSet().apply {
|
||||
putString(AccountValues.KEY_E164, E164_A)
|
||||
putString(AccountValues.KEY_ACI, ACI_A.toString())
|
||||
}
|
||||
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
|
||||
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** Verifying a case where a change number job is expected to be enqueued. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_highTrust_changedNumber() {
|
||||
val changeNumberListener = ChangeNumberListener()
|
||||
changeNumberListener.enqueue()
|
||||
|
||||
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
|
||||
changeNumberListener.waitForJobManager()
|
||||
assert(changeNumberListener.numberChangeWasEnqueued)
|
||||
}
|
||||
|
||||
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_merge_general() {
|
||||
// Setup
|
||||
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_B)
|
||||
|
||||
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
|
||||
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
|
||||
val smsId3: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
|
||||
|
||||
val mmsId1: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
|
||||
val mmsId2: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
|
||||
val mmsId3: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
|
||||
|
||||
val threadIdAci: Long = threadDatabase.getThreadIdFor(recipientIdAci)!!
|
||||
val threadIdE164: Long = threadDatabase.getThreadIdFor(recipientIdE164)!!
|
||||
assertNotEquals(threadIdAci, threadIdE164)
|
||||
|
||||
mentionDatabase.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1)))
|
||||
mentionDatabase.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1)))
|
||||
|
||||
groupReceiptDatabase.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3)
|
||||
|
||||
val identityKeyAci: IdentityKey = identityKey(1)
|
||||
val identityKeyE164: IdentityKey = identityKey(2)
|
||||
|
||||
identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
|
||||
identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
|
||||
|
||||
sessionDatabase.store(localAci, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
|
||||
|
||||
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
|
||||
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
|
||||
|
||||
val profile1: NotificationProfile = notificationProfile(name = "Test")
|
||||
val profile2: NotificationProfile = notificationProfile(name = "Test2")
|
||||
|
||||
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci)
|
||||
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164)
|
||||
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
|
||||
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
|
||||
|
||||
val distributionListId: DistributionListId = distributionListDatabase.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!!
|
||||
|
||||
// Merge
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
|
||||
assertEquals(recipientIdAci, retrievedId)
|
||||
|
||||
// Recipient validation
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(recipientIdE164)
|
||||
assertEquals(retrievedId, existingE164Recipient.id)
|
||||
|
||||
// Thread validation
|
||||
assertEquals(threadIdAci, retrievedThreadId)
|
||||
Assert.assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
|
||||
Assert.assertNull(threadDatabase.getThreadRecord(threadIdE164))
|
||||
|
||||
// SMS validation
|
||||
val sms1: MessageRecord = smsDatabase.getMessageRecord(smsId1)!!
|
||||
val sms2: MessageRecord = smsDatabase.getMessageRecord(smsId2)!!
|
||||
val sms3: MessageRecord = smsDatabase.getMessageRecord(smsId3)!!
|
||||
|
||||
assertEquals(retrievedId, sms1.recipient.id)
|
||||
assertEquals(retrievedId, sms2.recipient.id)
|
||||
assertEquals(retrievedId, sms3.recipient.id)
|
||||
|
||||
assertEquals(retrievedThreadId, sms1.threadId)
|
||||
assertEquals(retrievedThreadId, sms2.threadId)
|
||||
assertEquals(retrievedThreadId, sms3.threadId)
|
||||
|
||||
// MMS validation
|
||||
val mms1: MessageRecord = mmsDatabase.getMessageRecord(mmsId1)!!
|
||||
val mms2: MessageRecord = mmsDatabase.getMessageRecord(mmsId2)!!
|
||||
val mms3: MessageRecord = mmsDatabase.getMessageRecord(mmsId3)!!
|
||||
|
||||
assertEquals(retrievedId, mms1.recipient.id)
|
||||
assertEquals(retrievedId, mms2.recipient.id)
|
||||
assertEquals(retrievedId, mms3.recipient.id)
|
||||
|
||||
assertEquals(retrievedThreadId, mms1.threadId)
|
||||
assertEquals(retrievedThreadId, mms2.threadId)
|
||||
assertEquals(retrievedThreadId, mms3.threadId)
|
||||
|
||||
// Mention validation
|
||||
val mention1: MentionModel = getMention(mmsId1)
|
||||
assertEquals(retrievedId, mention1.recipientId)
|
||||
assertEquals(retrievedThreadId, mention1.threadId)
|
||||
|
||||
val mention2: MentionModel = getMention(mmsId2)
|
||||
assertEquals(retrievedId, mention2.recipientId)
|
||||
assertEquals(retrievedThreadId, mention2.threadId)
|
||||
|
||||
// Group receipt validation
|
||||
val groupReceipts: List<GroupReceiptDatabase.GroupReceiptInfo> = groupReceiptDatabase.getGroupReceiptInfo(mmsId1)
|
||||
assertEquals(retrievedId, groupReceipts[0].recipientId)
|
||||
assertEquals(retrievedId, groupReceipts[1].recipientId)
|
||||
|
||||
// Identity validation
|
||||
assertEquals(identityKeyAci, identityDatabase.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
|
||||
Assert.assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
|
||||
|
||||
// Session validation
|
||||
Assert.assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1)))
|
||||
|
||||
// Reaction validation
|
||||
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
|
||||
val reactionsMms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(mmsId1, true))
|
||||
|
||||
assertEquals(1, reactionsSms.size)
|
||||
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
|
||||
|
||||
assertEquals(1, reactionsMms.size)
|
||||
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
|
||||
|
||||
// Notification Profile validation
|
||||
val updatedProfile1: NotificationProfile = notificationProfileDatabase.getProfile(profile1.id)!!
|
||||
val updatedProfile2: NotificationProfile = notificationProfileDatabase.getProfile(profile2.id)!!
|
||||
|
||||
MatcherAssert.assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
|
||||
MatcherAssert.assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
|
||||
|
||||
// Distribution List validation
|
||||
val updatedList: DistributionListRecord = distributionListDatabase.getList(distributionListId)!!
|
||||
|
||||
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// Misc
|
||||
// ==============================================================
|
||||
|
||||
@Test
|
||||
fun createByE164SanityCheck() {
|
||||
// GIVEN one recipient
|
||||
val recipientId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
|
||||
// WHEN I retrieve one by E164
|
||||
val possible: Optional<RecipientId> = recipientDatabase.getByE164(E164_A)
|
||||
|
||||
// THEN I get it back, and it has the properties I expect
|
||||
assertTrue(possible.isPresent)
|
||||
assertEquals(recipientId, possible.get())
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertTrue(recipient.e164.isPresent)
|
||||
assertEquals(E164_A, recipient.e164.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createByUuidSanityCheck() {
|
||||
// GIVEN one recipient
|
||||
val recipientId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
|
||||
// WHEN I retrieve one by UUID
|
||||
val possible: Optional<RecipientId> = recipientDatabase.getByServiceId(ACI_A)
|
||||
|
||||
// THEN I get it back, and it has the properties I expect
|
||||
assertTrue(possible.isPresent)
|
||||
assertEquals(recipientId, possible.get())
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertTrue(recipient.serviceId.isPresent)
|
||||
assertEquals(ACI_A, recipient.serviceId.get())
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun getAndPossiblyMerge_noArgs_invalid() {
|
||||
recipientDatabase.getAndPossiblyMerge(null, null, true)
|
||||
}
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
|
||||
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
|
||||
}
|
||||
|
||||
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
|
||||
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty())
|
||||
}
|
||||
|
||||
private fun identityKey(value: Byte): IdentityKey {
|
||||
val bytes = ByteArray(33)
|
||||
bytes[0] = 0x05
|
||||
bytes[1] = value
|
||||
return IdentityKey(bytes)
|
||||
}
|
||||
|
||||
private fun notificationProfile(name: String): NotificationProfile {
|
||||
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
|
||||
}
|
||||
|
||||
private fun groupMasterKey(value: Byte): GroupMasterKey {
|
||||
val bytes = ByteArray(32)
|
||||
bytes[0] = value
|
||||
return GroupMasterKey(bytes)
|
||||
}
|
||||
|
||||
private fun decryptedGroup(members: Collection<UUID>): DecryptedGroup {
|
||||
return DecryptedGroup.newBuilder()
|
||||
.addAllMembers(members.map { DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(it)).build() })
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getMention(messageId: Long): MentionModel {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
return MentionModel(
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
|
||||
threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
|
||||
data class MentionModel(
|
||||
val recipientId: RecipientId,
|
||||
val threadId: Long
|
||||
)
|
||||
|
||||
private class ChangeNumberListener {
|
||||
|
||||
var numberChangeWasEnqueued = false
|
||||
private set
|
||||
|
||||
fun waitForJobManager() {
|
||||
ApplicationDependencies.getJobManager().flush()
|
||||
ThreadUtil.sleep(500)
|
||||
}
|
||||
|
||||
fun enqueue() {
|
||||
ApplicationDependencies.getJobManager().addListener(
|
||||
{ job -> job.factoryKey == RecipientChangedNumberJob.KEY },
|
||||
{ _, _ -> numberChangeWasEnqueued = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
|
||||
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
|
||||
|
||||
const val E164_A = "+12221234567"
|
||||
const val E164_B = "+13331234567"
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
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.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
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
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientDatabaseTest_processCdsV2Result {
|
||||
|
||||
private lateinit var recipientDatabase: RecipientDatabase
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
recipientDatabase = SignalDatabase.recipients
|
||||
|
||||
ensureDbEmpty()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processCdsV2Result_noMatch() {
|
||||
// Note that we haven't inserted any test data
|
||||
|
||||
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record: IdRecord = require(resultId)
|
||||
|
||||
assertEquals(resultId, record.id)
|
||||
assertEquals(E164_A, record.e164)
|
||||
assertEquals(ACI_A, record.sid)
|
||||
assertEquals(PNI_A, record.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processCdsV2Result_fullMatch() {
|
||||
val inputId: RecipientId = insert(E164_A, PNI_A, ACI_A)
|
||||
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record: IdRecord = require(resultId)
|
||||
|
||||
assertEquals(inputId, record.id)
|
||||
assertEquals(E164_A, record.e164)
|
||||
assertEquals(ACI_A, record.sid)
|
||||
assertEquals(PNI_A, record.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processCdsV2Result_onlyE164Matches() {
|
||||
val inputId: RecipientId = insert(E164_A, null, null)
|
||||
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record: IdRecord = require(resultId)
|
||||
|
||||
assertEquals(inputId, record.id)
|
||||
assertEquals(E164_A, record.e164)
|
||||
assertEquals(ACI_A, record.sid)
|
||||
assertEquals(PNI_A, record.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processCdsV2Result_e164AndPniMatches() {
|
||||
val inputId: RecipientId = insert(E164_A, PNI_A, null)
|
||||
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record: IdRecord = require(resultId)
|
||||
|
||||
assertEquals(inputId, record.id)
|
||||
assertEquals(E164_A, record.e164)
|
||||
assertEquals(ACI_A, record.sid)
|
||||
assertEquals(PNI_A, record.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processCdsV2Result_e164AndAciMatches() {
|
||||
val inputId: RecipientId = insert(E164_A, null, ACI_A)
|
||||
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record: IdRecord = require(resultId)
|
||||
|
||||
assertEquals(inputId, record.id)
|
||||
assertEquals(E164_A, record.e164)
|
||||
assertEquals(ACI_A, record.sid)
|
||||
assertEquals(PNI_A, record.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processCdsV2Result_onlyPniMatches() {
|
||||
val inputId: RecipientId = insert(null, PNI_A, null)
|
||||
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record: IdRecord = require(resultId)
|
||||
|
||||
assertEquals(inputId, record.id)
|
||||
assertEquals(E164_A, record.e164)
|
||||
assertEquals(ACI_A, record.sid)
|
||||
assertEquals(PNI_A, record.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processCdsV2Result_pniAndAciMatches() {
|
||||
val inputId: RecipientId = insert(null, PNI_A, ACI_A)
|
||||
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record: IdRecord = require(resultId)
|
||||
|
||||
assertEquals(inputId, record.id)
|
||||
assertEquals(E164_A, record.e164)
|
||||
assertEquals(ACI_A, record.sid)
|
||||
assertEquals(PNI_A, record.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processCdsV2Result_onlyAciMatches() {
|
||||
val inputId: RecipientId = insert(null, null, ACI_A)
|
||||
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record: IdRecord = require(resultId)
|
||||
|
||||
assertEquals(inputId, record.id)
|
||||
assertEquals(E164_A, record.e164)
|
||||
assertEquals(ACI_A, record.sid)
|
||||
assertEquals(PNI_A, record.pni)
|
||||
}
|
||||
|
||||
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientDatabase.TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
RecipientDatabase.PHONE to e164,
|
||||
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
|
||||
RecipientDatabase.PNI_COLUMN to pni?.toString(),
|
||||
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
|
||||
)
|
||||
)
|
||||
|
||||
return RecipientId.from(id)
|
||||
}
|
||||
|
||||
private fun require(id: RecipientId): IdRecord {
|
||||
return get(id)!!
|
||||
}
|
||||
|
||||
private fun get(id: RecipientId): IdRecord? {
|
||||
SignalDatabase.rawDatabase
|
||||
.select(RecipientDatabase.ID, RecipientDatabase.PHONE, RecipientDatabase.SERVICE_ID, RecipientDatabase.PNI_COLUMN)
|
||||
.from(RecipientDatabase.TABLE_NAME)
|
||||
.where("${RecipientDatabase.ID} = ?", id)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
IdRecord(
|
||||
id = RecipientId.from(cursor.requireLong(RecipientDatabase.ID)),
|
||||
e164 = cursor.requireString(RecipientDatabase.PHONE),
|
||||
sid = ServiceId.parseOrNull(cursor.requireString(RecipientDatabase.SERVICE_ID)),
|
||||
pni = PNI.parseOrNull(cursor.requireString(RecipientDatabase.PNI_COLUMN))
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
}
|
||||
|
||||
private data class IdRecord(
|
||||
val id: RecipientId,
|
||||
val e164: String?,
|
||||
val sid: ServiceId?,
|
||||
val pni: PNI?,
|
||||
)
|
||||
|
||||
companion object {
|
||||
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
|
||||
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
|
||||
|
||||
const val E164_A = "+12221234567"
|
||||
const val E164_B = "+13331234567"
|
||||
}
|
||||
}
|
||||
@@ -1,810 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.lang.AssertionError
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.IllegalStateException
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientDatabaseTest_processPnpTupleToChangeSet {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
|
||||
|
||||
private lateinit var db: RecipientDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.recipients
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_e164Only() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, null, null)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_e164AndPni() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_aciOnly() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(null, null, ACI_A)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun noMatch_pniOnly() {
|
||||
db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun noMatch_noData() {
|
||||
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_allFields() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullMatch() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, null, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_B, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndPniMatches_noExistingSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndPniMatches_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_noExistingSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_existingPniSession_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pniAndAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pniAndAciMatches_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAciMatches_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_aciOnly() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
Input(null, null, ACI_A)
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.thirdId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.thirdId,
|
||||
secondaryId = result.firstId
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
PnpOperation.SetAci(
|
||||
recipientId = result.firstId,
|
||||
aci = ACI_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniAndE164_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(E164_B, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(
|
||||
recipientId = result.firstId,
|
||||
pni = PNI_A
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_pniOnly_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.firstId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Input(E164_B, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(result.firstId, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null, pniSession = true),
|
||||
Input(E164_B, PNI_A, null, pniSession = true),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(result.firstId, PNI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.secondId),
|
||||
PnpOperation.SessionSwitchoverInsert(result.firstId)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_aciOnly() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(null, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_B, PNI_A, null),
|
||||
Input(null, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.firstId),
|
||||
PnpOperation.Update(
|
||||
recipientId = result.secondId,
|
||||
e164 = E164_A,
|
||||
pni = PNI_A,
|
||||
aci = ACI_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(E164_B, PNI_B, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.RemoveE164(result.secondId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.secondId,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164Aci_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(E164_B, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemoveE164(result.secondId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.secondId,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientDatabase.TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
RecipientDatabase.PHONE to e164,
|
||||
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
|
||||
RecipientDatabase.PNI_COLUMN to pni?.toString(),
|
||||
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
|
||||
)
|
||||
)
|
||||
|
||||
return RecipientId.from(id)
|
||||
}
|
||||
|
||||
private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
|
||||
SignalDatabase.rawDatabase.insert(
|
||||
SessionDatabase.TABLE_NAME, null,
|
||||
contentValuesOf(
|
||||
SessionDatabase.ACCOUNT_ID to account.toString(),
|
||||
SessionDatabase.ADDRESS to address.toString(),
|
||||
SessionDatabase.DEVICE to 1,
|
||||
SessionDatabase.RECORD to Util.getSecretBytes(32)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
|
||||
data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
|
||||
data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
|
||||
data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
|
||||
val id
|
||||
get() = if (ids.size == 1) {
|
||||
ids[0]
|
||||
} else {
|
||||
throw IllegalStateException("There are multiple IDs, but you assumed 1!")
|
||||
}
|
||||
|
||||
val firstId
|
||||
get() = ids[0]
|
||||
|
||||
val secondId
|
||||
get() = ids[1]
|
||||
|
||||
val thirdId
|
||||
get() = ids[2]
|
||||
}
|
||||
|
||||
private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
|
||||
return applyAndAssert(listOf(input), update, output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that will call insert your recipients, call [RecipientDatabase.processPnpTupleToChangeSet] with your params,
|
||||
* and then verify your output matches what you expect.
|
||||
*
|
||||
* It results the inserted ID's and changeset for additional verification.
|
||||
*
|
||||
* But basically this is here to make the tests more readable. It gives you a clear list of:
|
||||
* - input
|
||||
* - update
|
||||
* - output
|
||||
*
|
||||
* that you can spot check easily.
|
||||
*
|
||||
* Important: The output will only include records that contain fields from the input. That means
|
||||
* for:
|
||||
*
|
||||
* Input: E164_B, PNI_A, null
|
||||
* Update: E164_A, PNI_A, null
|
||||
*
|
||||
* You will get:
|
||||
* Output: E164_A, PNI_A, null
|
||||
*
|
||||
* Even though there was an update that will also result in the row (E164_B, null, null)
|
||||
*/
|
||||
private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
|
||||
val ids = input.map { insert(it.e164, it.pni, it.aci) }
|
||||
|
||||
input
|
||||
.filter { it.pniSession }
|
||||
.forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }
|
||||
|
||||
input
|
||||
.filter { it.aciSession }
|
||||
.forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }
|
||||
|
||||
val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
|
||||
val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
|
||||
val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }
|
||||
|
||||
val data = PnpDataSet(
|
||||
e164 = update.e164,
|
||||
pni = update.pni,
|
||||
aci = update.aci,
|
||||
byE164 = byE164,
|
||||
byPniSid = byPniSid,
|
||||
byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
|
||||
byAciSid = byAciSid,
|
||||
e164Record = byE164?.let { db.getRecord(it) },
|
||||
pniSidRecord = byPniSid?.let { db.getRecord(it) },
|
||||
aciSidRecord = byAciSid?.let { db.getRecord(it) }
|
||||
)
|
||||
val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)
|
||||
|
||||
val finalData = data.perform(changeSet.operations)
|
||||
|
||||
val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
|
||||
assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)
|
||||
|
||||
finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
|
||||
?: throw AssertionError("Expected output was not found in the result set! Expected: $output")
|
||||
|
||||
return PnpMatchResult(
|
||||
ids = ids,
|
||||
changeSet = changeSet
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
|
||||
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
|
||||
|
||||
const val E164_A = "+12221234567"
|
||||
const val E164_B = "+13331234567"
|
||||
}
|
||||
}
|
||||
@@ -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: RecipientDatabase
|
||||
private lateinit var sms: SmsDatabase
|
||||
|
||||
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.sms
|
||||
|
||||
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: MessageDatabase.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: MessageDatabase.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: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val latestMessage: MessageDatabase.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,463 +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.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 StorySendsDatabaseTest {
|
||||
|
||||
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: StorySendsDatabase
|
||||
|
||||
@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.mms.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.mms.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.mms.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.mms.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.mms.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
|
||||
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() {
|
||||
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)
|
||||
|
||||
assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete)
|
||||
}
|
||||
|
||||
@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.mms.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,74 +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.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class ThreadDatabaseTest_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.mms.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
val pinned = SignalDatabase.threads.pinnedThreadIds
|
||||
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.mms.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
val unarchivedCount = SignalDatabase.threads.unarchivedConversationListCount
|
||||
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.mms.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.threads.getUnarchivedConversationList(true, 0, 1).use {
|
||||
it.moveToFirst()
|
||||
assertEquals(threadId, CursorUtil.requireLong(it, ThreadDatabase.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,119 +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 org.junit.rules.ExternalResource
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
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.IdentityDatabase
|
||||
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.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.util.Util
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
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 : 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
|
||||
|
||||
override fun before() {
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
self = setupSelf()
|
||||
others = setupOthers()
|
||||
}
|
||||
|
||||
private fun setupSelf(): Recipient {
|
||||
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)
|
||||
|
||||
registrationRepository.registerAccountWithoutRegistrationLock(
|
||||
RegistrationData(
|
||||
code = "123123",
|
||||
e164 = "+15554045550101",
|
||||
password = Util.getSecret(18),
|
||||
registrationId = registrationRepository.registrationId,
|
||||
profileKey = registrationRepository.getProfileKey("+15554045550101"),
|
||||
fcmToken = null
|
||||
),
|
||||
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false)
|
||||
).blockingGet()
|
||||
|
||||
SignalStore.kbsValues().optOut()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete(application)
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
return Recipient.self()
|
||||
}
|
||||
|
||||
private fun setupOthers(): List<RecipientId> {
|
||||
val others = mutableListOf<RecipientId>()
|
||||
|
||||
for (i in 0..4) {
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
val recipientId = RecipientId.from(aci, "+1555555101$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))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey)
|
||||
others += recipientId
|
||||
}
|
||||
|
||||
return others
|
||||
}
|
||||
|
||||
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: IdentityDatabase.VerifiedStatus) {
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityDatabase.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.mms.deleteAllThreads()
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
@@ -64,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" />
|
||||
@@ -91,29 +92,19 @@
|
||||
<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"/>
|
||||
|
||||
<application android:name=".ApplicationContext"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
tools:replace="android:allowBackup"
|
||||
android:resizeableActivity="true"
|
||||
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="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
|
||||
|
||||
<meta-data android:name="android.supports_size_changes"
|
||||
android:value="true" />
|
||||
|
||||
<meta-data android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version" />
|
||||
|
||||
@@ -127,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"/>
|
||||
|
||||
@@ -162,29 +154,18 @@
|
||||
<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:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
@@ -207,7 +188,7 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
android:value=".service.DirectShareService" />
|
||||
|
||||
</activity>
|
||||
|
||||
@@ -224,7 +205,7 @@
|
||||
<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" />
|
||||
@@ -244,21 +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: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" />
|
||||
@@ -275,26 +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-alias>
|
||||
|
||||
<activity android:name=".conversation.ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
@@ -311,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"
|
||||
@@ -319,14 +274,22 @@
|
||||
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"
|
||||
@@ -334,7 +297,7 @@
|
||||
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"/>
|
||||
|
||||
@@ -365,96 +328,31 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.v2.MediaSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:launchMode="singleTop"
|
||||
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: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: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=".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/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".revealable.ViewOnceMessageActivity"
|
||||
@@ -471,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"/>
|
||||
|
||||
@@ -530,7 +427,6 @@
|
||||
|
||||
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
|
||||
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">
|
||||
@@ -549,25 +445,16 @@
|
||||
|
||||
<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.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"
|
||||
@@ -617,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"
|
||||
@@ -643,34 +531,10 @@
|
||||
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.FullScreenMedia" />
|
||||
|
||||
<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"/>
|
||||
|
||||
<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: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">
|
||||
<intent-filter>
|
||||
@@ -712,11 +576,16 @@
|
||||
<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.FcmFetchForegroundService" />
|
||||
<service android:name=".gcm.FcmFetchService" />
|
||||
|
||||
<service android:name=".gcm.FcmReceiveService">
|
||||
<intent-filter>
|
||||
@@ -773,18 +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.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"
|
||||
@@ -809,6 +686,26 @@
|
||||
|
||||
</provider>
|
||||
|
||||
<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"/>
|
||||
@@ -834,13 +731,6 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".messageprocessingalarm.MessageProcessReceiver">
|
||||
<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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
|
||||
|
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 |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 685 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 603 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 391 KiB |
@@ -60,14 +60,11 @@ import androidx.camera.core.impl.LensFacingConverter;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@@ -85,7 +82,7 @@ import java.util.concurrent.Executor;
|
||||
@RequiresApi(21)
|
||||
@SuppressLint("RestrictedApi")
|
||||
public final class SignalCameraView extends FrameLayout {
|
||||
static final String TAG = Log.tag(SignalCameraView.class);
|
||||
static final String TAG = SignalCameraView.class.getSimpleName();
|
||||
|
||||
static final int INDEFINITE_VIDEO_DURATION = -1;
|
||||
static final int INDEFINITE_VIDEO_SIZE = -1;
|
||||
@@ -131,11 +128,6 @@ public final class SignalCameraView extends FrameLayout {
|
||||
// For accessibility event
|
||||
private MotionEvent mUpEvent;
|
||||
|
||||
// BEGIN Custom Signal Code Block
|
||||
private Consumer<Throwable> errorConsumer;
|
||||
private Throwable pendingError;
|
||||
// END Custom Signal Code Block
|
||||
|
||||
public SignalCameraView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -173,32 +165,14 @@ public final class SignalCameraView extends FrameLayout {
|
||||
* androidx.lifecycle.Lifecycle.State#DESTROYED} state.
|
||||
* @throws IllegalStateException if camera permissions are not granted.
|
||||
*/
|
||||
// BEGIN Custom Signal Code Block
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner, Consumer<Throwable> errorConsumer) {
|
||||
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner) {
|
||||
mCameraModule.bindToLifecycle(lifecycleOwner);
|
||||
this.errorConsumer = errorConsumer;
|
||||
if (pendingError != null) {
|
||||
errorConsumer.accept(pendingError);
|
||||
}
|
||||
}
|
||||
// END Custom Signal Code Block
|
||||
|
||||
|
||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
|
||||
|
||||
// Begin custom signal code block
|
||||
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
|
||||
mCameraModule = new SignalCameraXModule(this, error -> {
|
||||
if (errorConsumer != null) {
|
||||
errorConsumer.accept(error);
|
||||
} else {
|
||||
pendingError = error;
|
||||
}
|
||||
});
|
||||
// End custom signal code block
|
||||
mCameraModule = new SignalCameraXModule(this);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
|
||||
|
||||
@@ -46,7 +46,6 @@ import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
@@ -124,9 +123,7 @@ final class SignalCameraXModule {
|
||||
@Nullable
|
||||
ProcessCameraProvider mCameraProvider;
|
||||
|
||||
// BEGIN Custom Signal Code Block
|
||||
SignalCameraXModule(SignalCameraView view, Consumer<Throwable> errorConsumer) {
|
||||
// END Custom Signal Code Block
|
||||
SignalCameraXModule(SignalCameraView view) {
|
||||
mCameraView = view;
|
||||
|
||||
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
|
||||
@@ -144,9 +141,7 @@ final class SignalCameraXModule {
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
// BEGIN Custom Signal Code Block
|
||||
errorConsumer.accept(t);
|
||||
// END Custom Signal Code Block
|
||||
throw new RuntimeException("CameraX failed to initialize.", t);
|
||||
}
|
||||
}, CameraXExecutors.mainThreadExecutor());
|
||||
|
||||
@@ -227,10 +222,17 @@ final class SignalCameraXModule {
|
||||
// End Signal Custom Code Block
|
||||
|
||||
Rational targetAspectRatio;
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
// End Signal Custom Code Block
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
|
||||
} else {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
}
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||
@@ -510,12 +512,7 @@ final class SignalCameraXModule {
|
||||
return rotationDegrees;
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeExperimentalUsageError")
|
||||
public void invalidateView() {
|
||||
if (mPreview != null) {
|
||||
mPreview.setTargetRotation(getDisplaySurfaceRotation()); // Fixes issue #10940 (rotation not updated on phones using "Legacy API")
|
||||
}
|
||||
|
||||
updateViewInfo();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.google.android.material.bottomsheet
|
||||
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Manually adjust the nested scrolling child for a given [BottomSheetBehavior].
|
||||
*/
|
||||
object BottomSheetBehaviorHack {
|
||||
fun setNestedScrollingChild(behavior: BottomSheetBehavior<FrameLayout>, view: View) {
|
||||
behavior.nestedScrollingChildRef = WeakReference(view)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public final class Log {
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().e(tag, message, null);
|
||||
e(tag, message, null);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
@@ -32,7 +32,7 @@ import java.util.List;
|
||||
*/
|
||||
public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||
|
||||
private static final String TAG = Log.tag(APNGDecoder.class);
|
||||
private static final String TAG = APNGDecoder.class.getSimpleName();
|
||||
|
||||
private APNGWriter apngWriter;
|
||||
private int mLoopCount;
|
||||
|
||||
@@ -21,7 +21,7 @@ import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
@@ -35,7 +35,7 @@ import java.util.Set;
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameAnimationDrawable<Decoder extends FrameSeqDecoder> extends Drawable implements Animatable2Compat, FrameSeqDecoder.RenderListener {
|
||||
private static final String TAG = Log.tag(FrameAnimationDrawable.class);
|
||||
private static final String TAG = FrameAnimationDrawable.class.getSimpleName();
|
||||
private final Paint paint = new Paint();
|
||||
private final Decoder frameSeqDecoder;
|
||||
private DrawFilter drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
|
||||
@@ -15,7 +15,7 @@ import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.executor.FrameDecoderExecutor;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
@@ -39,7 +39,7 @@ import java.util.concurrent.locks.LockSupport;
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
|
||||
private static final String TAG = Log.tag(FrameSeqDecoder.class);
|
||||
private static final String TAG = FrameSeqDecoder.class.getSimpleName();
|
||||
private final int taskId;
|
||||
|
||||
private final Loader mLoader;
|
||||
|
||||
@@ -8,18 +8,15 @@ public final class AppCapabilities {
|
||||
private AppCapabilities() {
|
||||
}
|
||||
|
||||
private static final boolean UUID_CAPABLE = false;
|
||||
private static final boolean GV2_CAPABLE = true;
|
||||
private static final boolean GV1_MIGRATION = true;
|
||||
private static final boolean ANNOUNCEMENT_GROUPS = true;
|
||||
private static final boolean SENDER_KEY = true;
|
||||
private static final boolean CHANGE_NUMBER = true;
|
||||
private static final boolean UUID_CAPABLE = false;
|
||||
private static final boolean GV2_CAPABLE = true;
|
||||
private static final boolean GV1_MIGRATION = true;
|
||||
|
||||
/**
|
||||
* @param storageCapable Whether or not the user can use storage service. This is another way of
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadges());
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.insights.InsightsOptOut;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
@@ -33,9 +32,10 @@ public final class AppInitialization {
|
||||
InsightsOptOut.userRequestedOptOut(context);
|
||||
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
|
||||
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, true);
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
|
||||
@@ -43,7 +43,6 @@ public final class AppInitialization {
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
}
|
||||
@@ -51,37 +50,10 @@ public final class AppInitialization {
|
||||
public static void onPostBackupRestore(@NonNull Context context) {
|
||||
Log.i(TAG, "onPostBackupRestore()");
|
||||
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onPostBackupRestore();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
SignalStore.onboarding().clearAll();
|
||||
TextSecurePreferences.onPostBackupRestore(context);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
EmojiSearchIndexDownloadJob.scheduleImmediately();
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary migration method that does the safest bits of {@link #onFirstEverAppLaunch(Context)}
|
||||
*/
|
||||
public static void onRepairFirstEverAppLaunch(@NonNull Context context) {
|
||||
Log.w(TAG, "onRepairFirstEverAppLaunch()");
|
||||
|
||||
InsightsOptOut.userRequestedOptOut(context);
|
||||
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
|
||||
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
}
|
||||
|
||||
@@ -16,95 +16,76 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.AndroidLogger;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.core.util.logging.PersistentLogger;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
|
||||
import org.thoughtcrime.securesms.logging.LogSecretProvider;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.tracing.Tracer;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.security.Security;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import rxdogtag2.RxDogTag;
|
||||
|
||||
/**
|
||||
* Will be called once when the TextSecure process is created.
|
||||
*
|
||||
@@ -113,11 +94,16 @@ import rxdogtag2.RxDogTag;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class ApplicationContext extends MultiDexApplication implements AppForegroundObserver.Listener {
|
||||
@Trace
|
||||
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
|
||||
|
||||
private static final String TAG = Log.tag(ApplicationContext.class);
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
|
||||
private PersistentLogger persistentLogger;
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private ViewOnceMessageManager viewOnceMessageManager;
|
||||
private PersistentLogger persistentLogger;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
public static ApplicationContext getInstance(Context context) {
|
||||
return (ApplicationContext)context.getApplicationContext();
|
||||
@@ -126,121 +112,83 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Tracer.getInstance().start("Application#onCreate()");
|
||||
AppStartup.getInstance().onApplicationCreate();
|
||||
SignalLocalMetrics.ColdStart.start();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
if (FeatureFlags.internalUser()) {
|
||||
Tracer.getInstance().setMaxBufferSize(35_000);
|
||||
}
|
||||
|
||||
super.onCreate();
|
||||
|
||||
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
|
||||
.addBlocking("sqlcipher-init", () -> {
|
||||
SqlCipherLibraryLoader.load();
|
||||
SignalDatabase.init(this,
|
||||
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
|
||||
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
|
||||
})
|
||||
.addBlocking("logging", () -> {
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
})
|
||||
.addBlocking("crash-handling", this::initializeCrashHandling)
|
||||
.addBlocking("rx-init", this::initializeRx)
|
||||
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
|
||||
.addBlocking("app-dependencies", this::initializeAppDependencies)
|
||||
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
|
||||
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
|
||||
.addBlocking("app-migrations", this::initializeApplicationMigrations)
|
||||
.addBlocking("ring-rtc", this::initializeRingRtc)
|
||||
.addBlocking("mark-registration", () -> RegistrationUtil.maybeMarkRegistrationComplete(this))
|
||||
.addBlocking("lifecycle-observer", () -> ApplicationDependencies.getAppForegroundObserver().addListener(this))
|
||||
.addBlocking("message-retriever", this::initializeMessageRetrieval)
|
||||
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
|
||||
.addBlocking("vector-compat", () -> {
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
})
|
||||
.addBlocking("proxy-init", () -> {
|
||||
if (SignalStore.proxy().isProxyEnabled()) {
|
||||
Log.w(TAG, "Proxy detected. Enabling Conscrypt.setUseEngineSocketByDefault()");
|
||||
Conscrypt.setUseEngineSocketByDefault(true);
|
||||
}
|
||||
})
|
||||
.addBlocking("blob-provider", this::initializeBlobProvider)
|
||||
.addBlocking("feature-flags", FeatureFlags::init)
|
||||
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
.addNonBlocking(this::initializeFcmCheck)
|
||||
.addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded)
|
||||
.addNonBlocking(this::initializePeriodicTasks)
|
||||
.addNonBlocking(this::initializeCircumvention)
|
||||
.addNonBlocking(this::initializePendingMessages)
|
||||
.addNonBlocking(this::initializeCleanup)
|
||||
.addNonBlocking(this::initializeGlideCodecs)
|
||||
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
|
||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
|
||||
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
|
||||
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
|
||||
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
|
||||
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
||||
.execute();
|
||||
initializeSecurityProvider();
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
initializeCrashHandling();
|
||||
initializeAppDependencies();
|
||||
initializeFirstEverAppLaunch();
|
||||
initializeApplicationMigrations();
|
||||
initializeMessageRetrieval();
|
||||
initializeExpiringMessageManager();
|
||||
initializeRevealableMessageManager();
|
||||
initializeGcmCheck();
|
||||
initializeSignedPreKeyCheck();
|
||||
initializePeriodicTasks();
|
||||
initializeCircumvention();
|
||||
initializeRingRtc();
|
||||
initializePendingMessages();
|
||||
initializeBlobProvider();
|
||||
initializeCleanup();
|
||||
initializeGlideCodecs();
|
||||
|
||||
FeatureFlags.init();
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
RegistrationUtil.maybeMarkRegistrationComplete(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
|
||||
DynamicTheme.setDefaultDayNightMode(this);
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
SignalLocalMetrics.ColdStart.onApplicationCreateFinished();
|
||||
Tracer.getInstance().end("Application#onCreate()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onForeground() {
|
||||
long startTime = System.currentTimeMillis();
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = true;
|
||||
Log.i(TAG, "App is now visible.");
|
||||
|
||||
ApplicationDependencies.getFrameRateTracker().start();
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
ApplicationDependencies.getDeadlockDetector().start();
|
||||
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getShakeToReport().enable();
|
||||
checkBuildExpiration();
|
||||
});
|
||||
|
||||
Log.d(TAG, "onStart() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
checkBuildExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackground() {
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = false;
|
||||
Log.i(TAG, "App is no longer visible.");
|
||||
KeyCachingService.onAppBackgrounded(this);
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
ApplicationDependencies.getFrameRateTracker().stop();
|
||||
ApplicationDependencies.getShakeToReport().disable();
|
||||
ApplicationDependencies.getDeadlockDetector().stop();
|
||||
ApplicationDependencies.getFrameRateTracker().end();
|
||||
}
|
||||
|
||||
public ExpiringMessageManager getExpiringMessageManager() {
|
||||
return expiringMessageManager;
|
||||
}
|
||||
|
||||
public ViewOnceMessageManager getViewOnceMessageManager() {
|
||||
return viewOnceMessageManager;
|
||||
}
|
||||
|
||||
public boolean isAppVisible() {
|
||||
return isAppVisible;
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
@@ -279,15 +227,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
|
||||
private void initializeLogging() {
|
||||
persistentLogger = new PersistentLogger(this);
|
||||
org.signal.core.util.logging.Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), persistentLogger);
|
||||
persistentLogger = new PersistentLogger(this, LogSecretProvider.getOrCreateAttachmentSecret(this), BuildConfig.VERSION_NAME);
|
||||
org.signal.core.util.logging.Log.initialize(new AndroidLogger(), persistentLogger);
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
Log.blockUntilAllWritesFinished();
|
||||
LogDatabase.getInstance(this).trimToSize();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeCrashHandling() {
|
||||
@@ -295,30 +238,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
Thread.setDefaultUncaughtExceptionHandler(new SignalUncaughtExceptionHandler(originalHandler));
|
||||
}
|
||||
|
||||
private void initializeRx() {
|
||||
RxDogTag.install();
|
||||
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
|
||||
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
|
||||
RxJavaPlugins.setErrorHandler(e -> {
|
||||
boolean wasWrapped = false;
|
||||
while ((e instanceof UndeliverableException || e instanceof AssertionError || e instanceof OnErrorNotImplementedException) && e.getCause() != null) {
|
||||
wasWrapped = true;
|
||||
e = e.getCause();
|
||||
}
|
||||
|
||||
if (wasWrapped && (e instanceof SocketException || e instanceof SocketTimeoutException || e instanceof InterruptedException)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.currentThread().getUncaughtExceptionHandler();
|
||||
if (uncaughtExceptionHandler == null) {
|
||||
uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
}
|
||||
|
||||
uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), e);
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeApplicationMigrations() {
|
||||
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
|
||||
}
|
||||
@@ -328,47 +247,43 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
|
||||
private void initializeAppDependencies() {
|
||||
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
|
||||
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this, new SignalServiceNetworkAccess(this)));
|
||||
}
|
||||
|
||||
private void initializeFirstEverAppLaunch() {
|
||||
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
|
||||
if (!SignalDatabase.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
|
||||
if (!SQLCipherOpenHelper.databaseFileExists(this)) {
|
||||
Log.i(TAG, "First ever app launch!");
|
||||
AppInitialization.onFirstEverAppLaunch(this);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
|
||||
TextSecurePreferences.setFirstInstallVersion(this, BuildConfig.CANONICAL_VERSION_CODE);
|
||||
} else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 90) {
|
||||
Log.i(TAG, "Detected a new install that doesn't have passphrases disabled -- assuming bad initialization.");
|
||||
AppInitialization.onRepairFirstEverAppLaunch(this);
|
||||
} else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 912) {
|
||||
Log.i(TAG, "Detected a not-recent install that doesn't have passphrases disabled -- disabling now.");
|
||||
TextSecurePreferences.setPasswordDisabled(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeFcmCheck() {
|
||||
if (SignalStore.account().isRegistered()) {
|
||||
long nextSetTime = SignalStore.account().getFcmTokenLastSetTime() + TimeUnit.HOURS.toMillis(6);
|
||||
private void initializeGcmCheck() {
|
||||
if (TextSecurePreferences.isPushRegistered(this)) {
|
||||
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
|
||||
|
||||
if (SignalStore.account().getFcmToken() == null || nextSetTime <= System.currentTimeMillis()) {
|
||||
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) {
|
||||
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSignedPreKeyCheck() {
|
||||
if (!TextSecurePreferences.isSignedPreKeyRegistered(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeExpiringMessageManager() {
|
||||
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
|
||||
this.expiringMessageManager = new ExpiringMessageManager(this);
|
||||
}
|
||||
|
||||
private void initializeRevealableMessageManager() {
|
||||
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializePendingRetryReceiptManager() {
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
this.viewOnceMessageManager = new ViewOnceMessageManager(this);
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
@@ -376,7 +291,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
LocalBackupListener.schedule(this);
|
||||
RotateSenderCertificateListener.schedule(this);
|
||||
MessageProcessReceiver.startOrUpdateAlarm(this);
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
@@ -385,28 +299,57 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
private void initializeRingRtc() {
|
||||
try {
|
||||
Set<String> HARDWARE_AEC_BLACKLIST = new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
add("Moto G5");
|
||||
add("Moto G (5S) Plus");
|
||||
add("Moto G4");
|
||||
add("TA-1053");
|
||||
add("Mi A1");
|
||||
add("Mi A2");
|
||||
add("E5823"); // Sony z5 compact
|
||||
add("Redmi Note 5");
|
||||
add("FP2"); // Fairphone FP2
|
||||
add("MI 5");
|
||||
}};
|
||||
|
||||
Set<String> OPEN_SL_ES_WHITELIST = new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
}};
|
||||
|
||||
if (HARDWARE_AEC_BLACKLIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
|
||||
}
|
||||
|
||||
if (!OPEN_SL_ES_WHITELIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
|
||||
}
|
||||
|
||||
CallManager.initialize(this, new RingRtcLogger());
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
throw new AssertionError("Unable to load ringrtc library", e);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void initializeCircumvention() {
|
||||
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, t);
|
||||
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, t);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void ensureProfileUploaded() {
|
||||
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
|
||||
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
|
||||
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
|
||||
}
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void executePendingContactSync() {
|
||||
@@ -421,26 +364,23 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
FcmJobService.schedule(this);
|
||||
} else {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(this));
|
||||
}
|
||||
TextSecurePreferences.setNeedsMessagePull(this, false);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void initializeBlobProvider() {
|
||||
BlobProvider.getInstance().initialize(this);
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
BlobProvider.getInstance().onSessionStart(this);
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void cleanAvatarStorage() {
|
||||
AvatarPickerStorage.cleanOrphans(this);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void initializeCleanup() {
|
||||
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeGlideCodecs() {
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013-2017 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.help.HelpFragment;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
/**
|
||||
* The Activity for application preference display and management.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
|
||||
|
||||
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
|
||||
private static final String PREFERENCE_CATEGORY_USERNAME = "preference_category_username";
|
||||
private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms";
|
||||
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
|
||||
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
|
||||
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
|
||||
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
||||
private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage";
|
||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
||||
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
||||
|
||||
private static final String WAS_CONFIGURATION_UPDATED = "was_configuration_updated";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private boolean wasConfigurationUpdated = false;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
//noinspection ConstantConditions
|
||||
this.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
|
||||
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
|
||||
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
|
||||
initFragment(android.R.id.content, new BackupsPreferenceFragment());
|
||||
} else if (icicle == null) {
|
||||
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
|
||||
} else {
|
||||
wasConfigurationUpdated = icicle.getBoolean(WAS_CONFIGURATION_UPDATED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putBoolean(WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data)
|
||||
{
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(android.R.id.content);
|
||||
fragment.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
if (fragmentManager.getBackStackEntryCount() > 0) {
|
||||
fragmentManager.popBackStack();
|
||||
} else {
|
||||
if (wasConfigurationUpdated) {
|
||||
setResult(MainActivity.RESULT_CONFIG_CHANGED);
|
||||
} else {
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
onSupportNavigateUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(TextSecurePreferences.THEME_PREF)) {
|
||||
DynamicTheme.setDefaultDayNightMode(this);
|
||||
recreate();
|
||||
} else if (key.equals(TextSecurePreferences.LANGUAGE_PREF)) {
|
||||
CachedInflater.from(this).clear();
|
||||
wasConfigurationUpdated = true;
|
||||
recreate();
|
||||
|
||||
Intent intent = new Intent(this, KeyCachingService.class);
|
||||
intent.setAction(KeyCachingService.LOCALE_CHANGE_EVENT);
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
|
||||
public void pushFragment(@NonNull Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(android.R.id.content, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
|
||||
.setOnPreferenceClickListener(new ProfileClickListener());
|
||||
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
||||
.setOnPreferenceClickListener(new UsernameClickListener());
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APP_PROTECTION));
|
||||
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_STORAGE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_STORAGE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DONATE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
|
||||
|
||||
tintIcons();
|
||||
}
|
||||
|
||||
private void tintIcons() {
|
||||
if (Build.VERSION.SDK_INT >= 21) return;
|
||||
|
||||
Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS);
|
||||
preference.getIcon().setColorFilter(ContextCompat.getColor(requireContext(), R.color.signal_icon_tint_primary), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
if (FeatureFlags.usernames()) {
|
||||
UsernamePreference pref = (UsernamePreference) findPreference(PREFERENCE_CATEGORY_USERNAME);
|
||||
pref.setVisible(shouldDisplayUsernameReminder());
|
||||
pref.setOnLongClickListener(v -> {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.ApplicationPreferencesActivity_hide_reminder)
|
||||
.setPositiveButton(R.string.ApplicationPreferencesActivity_hide, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
SignalStore.misc().hideUsernameReminder();
|
||||
findPreference(PREFERENCE_CATEGORY_USERNAME).setVisible(false);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
//noinspection ConstantConditions
|
||||
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.text_secure_normal__menu_settings);
|
||||
setCategorySummaries();
|
||||
setCategoryVisibility();
|
||||
}
|
||||
|
||||
private void setCategorySummaries() {
|
||||
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
|
||||
|
||||
if (FeatureFlags.usernames()) {
|
||||
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
||||
.setVisible(shouldDisplayUsernameReminder());
|
||||
}
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||
.setSummary(SmsMmsPreferenceFragment.getSummary(getActivity()));
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
.setSummary(NotificationsPreferenceFragment.getSummary(getActivity()));
|
||||
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
|
||||
.setSummary(AppProtectionPreferenceFragment.getSummary(getActivity()));
|
||||
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
|
||||
.setSummary(AppearancePreferenceFragment.getSummary(getActivity()));
|
||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
||||
.setSummary(ChatsPreferenceFragment.getSummary(getActivity()));
|
||||
}
|
||||
|
||||
private void setCategoryVisibility() {
|
||||
Preference devicePreference = this.findPreference(PREFERENCE_CATEGORY_DEVICES);
|
||||
if (devicePreference != null && !TextSecurePreferences.isPushRegistered(getActivity())) {
|
||||
getPreferenceScreen().removePreference(devicePreference);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldDisplayUsernameReminder() {
|
||||
return FeatureFlags.usernames() && !Recipient.self().getUsername().isPresent() && SignalStore.misc().shouldShowUsernameReminder();
|
||||
}
|
||||
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
||||
private String category;
|
||||
|
||||
CategoryClickListener(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Fragment fragment = null;
|
||||
|
||||
switch (category) {
|
||||
case PREFERENCE_CATEGORY_SMS_MMS:
|
||||
fragment = new SmsMmsPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_NOTIFICATIONS:
|
||||
fragment = new NotificationsPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_APP_PROTECTION:
|
||||
fragment = new AppProtectionPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_APPEARANCE:
|
||||
fragment = new AppearancePreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_CHATS:
|
||||
fragment = new ChatsPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_STORAGE:
|
||||
fragment = new StoragePreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DEVICES:
|
||||
Intent intent = new Intent(getActivity(), DeviceActivity.class);
|
||||
startActivity(intent);
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_ADVANCED:
|
||||
fragment = new AdvancedPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_HELP:
|
||||
fragment = new HelpFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DONATE:
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (fragment != null) {
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
|
||||
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
@@ -28,7 +27,6 @@ import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
@@ -59,12 +57,6 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, from, "avatar").toBundle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
@@ -79,14 +71,12 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
|
||||
}
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
EmojiTextView title = findViewById(R.id.title);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
requireSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
|
||||
Context context = getApplicationContext();
|
||||
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
||||
@@ -132,14 +122,14 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
}
|
||||
});
|
||||
|
||||
title.setText(recipient.getDisplayName(context));
|
||||
toolbar.setTitle(recipient.getDisplayName(context));
|
||||
});
|
||||
|
||||
FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
|
||||
|
||||
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
|
||||
|
||||
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), toolbar);
|
||||
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||
|
||||
fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.ConfigurationUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
@@ -33,10 +31,8 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||
logEvent("onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
AppStartup.getInstance().onCriticalRenderEventEnd();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,7 +44,6 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onStart() {
|
||||
logEvent("onStart()");
|
||||
ApplicationDependencies.getShakeToReport().registerActivity(this);
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@@ -86,8 +81,7 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode()
|
||||
: AppCompatDelegate.getDefaultNightMode();
|
||||
|
||||
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
|
||||
configuration.orientation = Configuration.ORIENTATION_UNDEFINED;
|
||||
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
|
||||
|
||||
applyOverrideConfiguration(configuration);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,8 @@ import androidx.lifecycle.Observer;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
@@ -26,49 +20,31 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable, Multiselectable {
|
||||
public interface BindableConversationItem extends Unbindable {
|
||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MultiselectPart> batchSelected,
|
||||
@NonNull Set<ConversationMessage> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseMention,
|
||||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
boolean canPlayInline,
|
||||
@NonNull Colorizer colorizer,
|
||||
boolean isCondensedMode);
|
||||
boolean pulseMention);
|
||||
|
||||
@NonNull ConversationMessage getConversationMessage();
|
||||
ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
default void updateTimestamps() {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
default void updateContactNameColor() {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
default void updateSelectedState() {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
void onQuotedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms);
|
||||
void onStickerClicked(@NonNull StickerLocator stickerLocator);
|
||||
void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord);
|
||||
@@ -76,37 +52,18 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms);
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
|
||||
void onIncomingIdentityMismatchClicked(@NonNull RecipientId recipientId);
|
||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onVoiceNotePause(@NonNull Uri uri);
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
|
||||
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
|
||||
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
|
||||
void onChatSessionRefreshLearnMoreClicked();
|
||||
void onBadDecryptLearnMoreClicked(@NonNull RecipientId author);
|
||||
void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient);
|
||||
void onJoinGroupCallClicked();
|
||||
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
|
||||
void onEnableCallNotificationsClicked();
|
||||
void onPlayInlineContent(ConversationMessage conversationMessage);
|
||||
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
|
||||
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
|
||||
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
|
||||
void onCallToAction(@NonNull String action);
|
||||
void onDonateClicked();
|
||||
void onBlockJoinRequest(@NonNull Recipient recipient);
|
||||
void onRecipientNameClicked(@NonNull RecipientId target);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
|
||||
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
@@ -12,12 +10,11 @@ import java.util.Set;
|
||||
|
||||
public interface BindableConversationListItem extends Unbindable {
|
||||
|
||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ThreadRecord thread,
|
||||
void bind(@NonNull ThreadRecord thread,
|
||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull ConversationSet selectedConversations);
|
||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||
|
||||
void setSelectedConversations(@NonNull ConversationSet conversations);
|
||||
void setBatchMode(boolean batchMode);
|
||||
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,9 @@ import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
/**
|
||||
* This should be used whenever we want to prompt the user to block/unblock a recipient.
|
||||
@@ -32,15 +30,15 @@ public final class BlockUnblockDialog {
|
||||
AlertDialog.Builder::show);
|
||||
}
|
||||
|
||||
public static void showBlockAndReportSpamFor(@NonNull Context context,
|
||||
@NonNull Lifecycle lifecycle,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull Runnable onBlock,
|
||||
@NonNull Runnable onBlockAndReportSpam)
|
||||
public static void showBlockAndDeleteFor(@NonNull Context context,
|
||||
@NonNull Lifecycle lifecycle,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull Runnable onBlock,
|
||||
@NonNull Runnable onBlockAndDelete)
|
||||
{
|
||||
SimpleTask.run(lifecycle,
|
||||
() -> buildBlockFor(context, recipient, onBlock, onBlockAndReportSpam),
|
||||
AlertDialog.Builder::show);
|
||||
() -> buildBlockFor(context, recipient, onBlock, onBlockAndDelete),
|
||||
AlertDialog.Builder::show);
|
||||
}
|
||||
|
||||
public static void showUnblockFor(@NonNull Context context,
|
||||
@@ -57,15 +55,15 @@ public final class BlockUnblockDialog {
|
||||
private static AlertDialog.Builder buildBlockFor(@NonNull Context context,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull Runnable onBlock,
|
||||
@Nullable Runnable onBlockAndReportSpam)
|
||||
@Nullable Runnable onBlockAndDelete)
|
||||
{
|
||||
recipient = recipient.resolve();
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
Resources resources = context.getResources();
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
|
||||
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
|
||||
@@ -76,20 +74,14 @@ public final class BlockUnblockDialog {
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_block, ((dialog, which) -> onBlock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
} else if (recipient.isReleaseNotes()) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_block_getting_signal_updates_and_news);
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
} else {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(recipient.isRegistered() ? R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages
|
||||
: R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_send_you_messages);
|
||||
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
|
||||
|
||||
if (onBlockAndReportSpam != null) {
|
||||
if (onBlockAndDelete != null) {
|
||||
builder.setNeutralButton(android.R.string.cancel, null);
|
||||
builder.setNegativeButton(R.string.BlockUnblockDialog_report_spam_and_block, (d, w) -> onBlockAndReportSpam.run());
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run());
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_delete, (d, w) -> onBlockAndDelete.run());
|
||||
builder.setNegativeButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run());
|
||||
} else {
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
@@ -106,11 +98,11 @@ public final class BlockUnblockDialog {
|
||||
{
|
||||
recipient = recipient.resolve();
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
Resources resources = context.getResources();
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
|
||||
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
@@ -121,16 +113,9 @@ public final class BlockUnblockDialog {
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
} else if (recipient.isReleaseNotes()) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_resume_getting_signal_updates_and_news);
|
||||
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
} else {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(recipient.isRegistered() ? R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other
|
||||
: R.string.BlockUnblockDialog_you_will_be_able_to_message_each_other);
|
||||
builder.setMessage(R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other);
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ConfirmIdentityDialog.class.getSimpleName();
|
||||
|
||||
private OnClickListener callback;
|
||||
|
||||
public ConfirmIdentityDialog(Context context,
|
||||
MessageRecord messageRecord,
|
||||
IdentityKeyMismatch mismatch)
|
||||
{
|
||||
super(context);
|
||||
|
||||
Recipient recipient = Recipient.resolved(mismatch.getRecipientId(context));
|
||||
String name = recipient.getDisplayName(context);
|
||||
String introduction = context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " +
|
||||
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact));
|
||||
|
||||
spannableString.setSpan(new VerifySpan(context, mismatch),
|
||||
introduction.length()+1, spannableString.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
setTitle(name);
|
||||
setMessage(spannableString);
|
||||
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.ConfirmIdentityDialog_accept), new AcceptListener(messageRecord, mismatch, recipient.getId()));
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), new CancelListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
((TextView)this.findViewById(android.R.id.message))
|
||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
public void setCallback(OnClickListener callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
private class AcceptListener implements OnClickListener {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
private final IdentityKeyMismatch mismatch;
|
||||
private final RecipientId recipientId;
|
||||
|
||||
private AcceptListener(MessageRecord messageRecord, IdentityKeyMismatch mismatch, RecipientId recipientId) {
|
||||
this.messageRecord = messageRecord;
|
||||
this.mismatch = mismatch;
|
||||
this.recipientId = recipientId;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new AsyncTask<Void, Void, Void>()
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(Recipient.resolved(recipientId).requireServiceId(), 1);
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(getContext());
|
||||
|
||||
identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true);
|
||||
}
|
||||
|
||||
processMessageRecord(messageRecord);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void processMessageRecord(MessageRecord messageRecord) {
|
||||
if (messageRecord.isOutgoing()) processOutgoingMessageRecord(messageRecord);
|
||||
else processIncomingMessageRecord(messageRecord);
|
||||
}
|
||||
|
||||
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
||||
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
if (messageRecord.getRecipient().isPushGroup()) {
|
||||
MessageSender.resendGroupMessage(getContext(), messageRecord, Recipient.resolved(mismatch.getRecipientId(getContext())).getId());
|
||||
} else {
|
||||
MessageSender.resend(getContext(), messageRecord);
|
||||
}
|
||||
} else {
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
MessageSender.resend(getContext(), messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private void processIncomingMessageRecord(MessageRecord messageRecord) {
|
||||
try {
|
||||
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
||||
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
boolean legacy = !messageRecord.isContentBundleKeyExchange();
|
||||
|
||||
SignalServiceEnvelope envelope = new SignalServiceEnvelope(SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE,
|
||||
Optional.of(RecipientUtil.toSignalServiceAddress(getContext(), messageRecord.getIndividualRecipient())),
|
||||
messageRecord.getRecipientDeviceId(),
|
||||
messageRecord.getDateSent(),
|
||||
legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
!legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
0,
|
||||
0,
|
||||
null);
|
||||
|
||||
long pushId = pushDatabase.insert(envelope);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new PushDecryptMessageJob(getContext(), pushId, messageRecord.getId()));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
if (callback != null) callback.onClick(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class CancelListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (callback != null) callback.onClick(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,24 +20,21 @@ import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Base activity container for selecting a list of contacts.
|
||||
@@ -50,7 +47,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.ScrollCallback
|
||||
{
|
||||
private static final String TAG = Log.tag(ContactSelectionActivity.class);
|
||||
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
|
||||
|
||||
public static final String EXTRA_LAYOUT_RES_ID = "layout_res_id";
|
||||
|
||||
@@ -58,8 +55,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
|
||||
protected ContactSelectionListFragment contactsFragment;
|
||||
|
||||
private Toolbar toolbar;
|
||||
private ContactFilterView contactFilterView;
|
||||
private ContactFilterToolbar toolbar;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
@@ -69,14 +65,13 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int displayMode = Util.isDefaultSmsProvider(this) ? DisplayMode.FLAG_ALL
|
||||
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
|
||||
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
}
|
||||
|
||||
setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity));
|
||||
|
||||
initializeContactFilterView();
|
||||
initializeToolbar();
|
||||
initializeResources();
|
||||
initializeSearch();
|
||||
@@ -88,34 +83,28 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
protected Toolbar getToolbar() {
|
||||
protected ContactFilterToolbar getToolbar() {
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
protected ContactFilterView getContactFilterView() {
|
||||
return contactFilterView;
|
||||
}
|
||||
|
||||
private void initializeContactFilterView() {
|
||||
this.contactFilterView = findViewById(R.id.contact_filter_edit_text);
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
this.toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
getSupportActionBar().setIcon(null);
|
||||
getSupportActionBar().setLogo(null);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
contactsFragment.setOnRefreshListener(this);
|
||||
}
|
||||
|
||||
private void initializeSearch() {
|
||||
contactFilterView.setOnFilterChangedListener(filter -> contactsFragment.setQueryFilter(filter));
|
||||
toolbar.setOnFilterChangedListener(filter -> contactsFragment.setQueryFilter(filter));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,12 +113,12 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
callback.accept(true);
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {}
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
||||
|
||||
@Override
|
||||
public void onBeginScroll() {
|
||||
@@ -153,7 +142,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
@Override
|
||||
protected Void doInBackground(Context... params) {
|
||||
try {
|
||||
ContactDiscovery.refreshAll(params[0], true);
|
||||
DirectoryHelper.refreshDirectory(params[0], true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
@@ -166,7 +155,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
ContactSelectionActivity activity = this.activity.get();
|
||||
|
||||
if (activity != null && !activity.isFinishing()) {
|
||||
activity.contactFilterView.clear();
|
||||
activity.toolbar.clear();
|
||||
activity.contactsFragment.resetQueryFilter();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.LayoutTransition;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Rect;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -31,17 +31,16 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
@@ -53,64 +52,54 @@ import androidx.transition.TransitionManager;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.WarningTextView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactChip;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContacts;
|
||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public final class ContactSelectionListFragment extends LoggingFragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||
|
||||
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 0;
|
||||
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
|
||||
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
|
||||
|
||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||
@@ -121,43 +110,32 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public static final String SELECTION_LIMITS = "selection_limits";
|
||||
public static final String CURRENT_SELECTION = "current_selection";
|
||||
public static final String HIDE_COUNT = "hide_count";
|
||||
public static final String CAN_SELECT_SELF = "can_select_self";
|
||||
public static final String DISPLAY_CHIPS = "display_chips";
|
||||
public static final String RV_PADDING_BOTTOM = "recycler_view_padding_bottom";
|
||||
public static final String RV_CLIP = "recycler_view_clipping";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private RecyclerView chipRecycler;
|
||||
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
||||
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
||||
private MappingAdapter contactChipAdapter;
|
||||
private ContactChipViewModel contactChipViewModel;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
|
||||
private HeaderActionProvider headerActionProvider;
|
||||
private TextView headerActionView;
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private WarningTextView groupLimit;
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@Nullable private ListCallback listCallback;
|
||||
@Nullable private ScrollCallback scrollCallback;
|
||||
private GlideRequests glideRequests;
|
||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
private boolean canSelectSelf;
|
||||
private GlideRequests glideRequests;
|
||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -167,45 +145,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
listCallback = (ListCallback) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof ScrollCallback) {
|
||||
scrollCallback = (ScrollCallback) getParentFragment();
|
||||
}
|
||||
|
||||
if (context instanceof ScrollCallback) {
|
||||
scrollCallback = (ScrollCallback) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof OnContactSelectedListener) {
|
||||
onContactSelectedListener = (OnContactSelectedListener) getParentFragment();
|
||||
}
|
||||
|
||||
if (context instanceof OnContactSelectedListener) {
|
||||
onContactSelectedListener = (OnContactSelectedListener) context;
|
||||
}
|
||||
|
||||
if (context instanceof OnSelectionLimitReachedListener) {
|
||||
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof OnSelectionLimitReachedListener) {
|
||||
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) getParentFragment();
|
||||
}
|
||||
|
||||
if (context instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
|
||||
}
|
||||
|
||||
if (context instanceof HeaderActionProvider) {
|
||||
headerActionProvider = (HeaderActionProvider) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof HeaderActionProvider) {
|
||||
headerActionProvider = (HeaderActionProvider) getParentFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -234,7 +176,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
|
||||
if (safeArguments().getBoolean(RECENTS, activity.getIntent().getBooleanExtra(RECENTS, false))) {
|
||||
if (activity.getIntent().getBooleanExtra(RECENTS, false)) {
|
||||
LoaderManager.getInstance(this).initLoader(0, null, ContactSelectionListFragment.this);
|
||||
} else {
|
||||
initializeNoContactsPermission();
|
||||
@@ -255,13 +197,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
||||
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
||||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
chipRecycler = view.findViewById(R.id.chipRecycler);
|
||||
chipGroup = view.findViewById(R.id.chipGroup);
|
||||
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
|
||||
groupLimit = view.findViewById(R.id.group_limit);
|
||||
constraintLayout = view.findViewById(R.id.container);
|
||||
headerActionView = view.findViewById(R.id.header_action);
|
||||
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
|
||||
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
recyclerView.setItemAnimator(new DefaultItemAnimator() {
|
||||
@Override
|
||||
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
@@ -269,41 +210,13 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
});
|
||||
|
||||
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
|
||||
contactChipAdapter = new MappingAdapter();
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
Intent intent = requireActivity().getIntent();
|
||||
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
SelectedContacts.register(contactChipAdapter, this::onChipCloseIconClicked);
|
||||
chipRecycler.setAdapter(contactChipAdapter);
|
||||
swipeRefresh.setEnabled(intent.getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
Disposable disposable = contactChipViewModel.getState().subscribe(this::handleSelectedContactsChanged);
|
||||
|
||||
lifecycleDisposable.add(disposable);
|
||||
|
||||
Intent intent = requireActivity().getIntent();
|
||||
Bundle arguments = safeArguments();
|
||||
|
||||
int recyclerViewPadBottom = arguments.getInt(RV_PADDING_BOTTOM, intent.getIntExtra(RV_PADDING_BOTTOM, -1));
|
||||
boolean recyclerViewClipping = arguments.getBoolean(RV_CLIP, intent.getBooleanExtra(RV_CLIP, true));
|
||||
|
||||
if (recyclerViewPadBottom != -1) {
|
||||
ViewUtil.setPaddingBottom(recyclerView, recyclerViewPadBottom);
|
||||
}
|
||||
|
||||
recyclerView.setClipToPadding(recyclerViewClipping);
|
||||
|
||||
boolean isRefreshable = arguments.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, true));
|
||||
swipeRefresh.setNestedScrollingEnabled(isRefreshable);
|
||||
swipeRefresh.setEnabled(isRefreshable);
|
||||
|
||||
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
|
||||
selectionLimit = arguments.getParcelable(SELECTION_LIMITS);
|
||||
if (selectionLimit == null) {
|
||||
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
||||
}
|
||||
isMulti = selectionLimit != null;
|
||||
canSelectSelf = arguments.getBoolean(CAN_SELECT_SELF, intent.getBooleanExtra(CAN_SELECT_SELF, !isMulti));
|
||||
hideCount = intent.getBooleanExtra(HIDE_COUNT, false);
|
||||
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
||||
isMulti = selectionLimit != null;
|
||||
|
||||
if (!isMulti) {
|
||||
selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
@@ -311,45 +224,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
final HeaderAction headerAction;
|
||||
if (headerActionProvider != null) {
|
||||
headerAction = headerActionProvider.getHeaderAction();
|
||||
|
||||
headerActionView.setEnabled(true);
|
||||
headerActionView.setText(headerAction.getLabel());
|
||||
headerActionView.setCompoundDrawablesRelativeWithIntrinsicBounds(headerAction.getIcon(), 0, 0, 0);
|
||||
headerActionView.setOnClickListener(v -> headerAction.getAction().run());
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
|
||||
private final Rect bounds = new Rect();
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (hideLetterHeaders()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int firstPosition = layoutManager.findFirstVisibleItemPosition();
|
||||
if (firstPosition == 0) {
|
||||
View firstChild = recyclerView.getChildAt(0);
|
||||
recyclerView.getDecoratedBoundsWithMargins(firstChild, bounds);
|
||||
headerActionView.setTranslationY(bounds.top);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
headerActionView.setEnabled(false);
|
||||
}
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private @NonNull Bundle safeArguments() {
|
||||
return getArguments() != null ? getArguments() : new Bundle();
|
||||
private void updateGroupLimit(int chipCount) {
|
||||
int members = currentSelection.size() + chipCount;
|
||||
groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
|
||||
groupLimit.setVisibility(isMulti && !hideCount ? View.VISIBLE : View.GONE);
|
||||
groupLimit.setWarning(selectionWarningLimitExceeded());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -373,19 +257,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
||||
}
|
||||
|
||||
public int getTotalMemberCount() {
|
||||
if (cursorRecyclerViewAdapter == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return cursorRecyclerViewAdapter.getSelectedContactsCount() + cursorRecyclerViewAdapter.getCurrentContactsCount();
|
||||
}
|
||||
|
||||
private Set<RecipientId> getCurrentSelection() {
|
||||
List<RecipientId> currentSelection = safeArguments().getParcelableArrayList(CURRENT_SELECTION);
|
||||
if (currentSelection == null) {
|
||||
currentSelection = requireActivity().getIntent().getParcelableArrayListExtra(CURRENT_SELECTION);
|
||||
}
|
||||
List<RecipientId> currentSelection = requireActivity().getIntent().getParcelableArrayListExtra(CURRENT_SELECTION);
|
||||
|
||||
return currentSelection == null ? Collections.emptySet()
|
||||
: Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet()));
|
||||
@@ -403,8 +276,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti,
|
||||
currentSelection,
|
||||
safeArguments().getInt(ContactSelectionArguments.CHECKBOX_RESOURCE, R.drawable.contact_selection_checkbox));
|
||||
currentSelection);
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
@@ -422,8 +294,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
concatenateAdapter.addAdapter(footerAdapter);
|
||||
}
|
||||
|
||||
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
|
||||
recyclerView.setAdapter(concatenateAdapter);
|
||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
@@ -434,14 +306,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hideLetterHeaders() {
|
||||
return hasQueryFilter() || shouldDisplayRecents();
|
||||
}
|
||||
|
||||
private View createInviteActionView(@NonNull ListCallback listCallback) {
|
||||
@@ -506,21 +370,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
}
|
||||
|
||||
public void setRecyclerViewPaddingBottom(@Px int paddingBottom) {
|
||||
ViewUtil.setPaddingBottom(recyclerView, paddingBottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
FragmentActivity activity = requireActivity();
|
||||
int displayMode = safeArguments().getInt(DISPLAY_MODE, activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL));
|
||||
boolean displayRecents = shouldDisplayRecents();
|
||||
|
||||
if (cursorFactoryProvider != null) {
|
||||
return cursorFactoryProvider.get().create();
|
||||
} else {
|
||||
return new ContactsCursorLoader.Factory(activity, displayMode, cursorFilter, displayRecents).create();
|
||||
}
|
||||
FragmentActivity activity = requireActivity();
|
||||
return new ContactsCursorLoader(activity,
|
||||
activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL),
|
||||
cursorFilter, activity.getIntent().getBooleanExtra(RECENTS, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -552,23 +407,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
fastScroller.setRecyclerView(null);
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (headerActionView.isEnabled() && !hasQueryFilter()) {
|
||||
headerActionView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
headerActionView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
cursorRecyclerViewAdapter.changeCursor(null);
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
headerActionView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private boolean shouldDisplayRecents() {
|
||||
return safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@@ -589,7 +433,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
ContactDiscovery.refreshAll(context, false);
|
||||
DirectoryHelper.refreshDirectory(context, false);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
@@ -604,67 +448,27 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
reset();
|
||||
} else {
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the caller to submit a list of recipients to be marked selected. Useful for when a screen needs to load preselected
|
||||
* entries in the background before setting them in the adapter.
|
||||
*
|
||||
* @param contacts List of the contacts to select. This will not overwrite the current selection, but append to it.
|
||||
*/
|
||||
public void markSelected(@NonNull Set<ShareContact> contacts) {
|
||||
if (contacts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<SelectedContact> toMarkSelected = contacts.stream()
|
||||
.map(contact -> {
|
||||
if (contact.getRecipientId().isPresent()) {
|
||||
return SelectedContact.forRecipientId(contact.getRecipientId().get());
|
||||
} else {
|
||||
return SelectedContact.forPhone(null, contact.getNumber());
|
||||
}
|
||||
})
|
||||
.filter(c -> !cursorRecyclerViewAdapter.isSelectedContact(c))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
if (toMarkSelected.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final SelectedContact selectedContact : toMarkSelected) {
|
||||
markContactSelected(selectedContact);
|
||||
}
|
||||
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount());
|
||||
}
|
||||
|
||||
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(ContactSelectionListItem contact) {
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
|
||||
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (selectionHardLimitReached()) {
|
||||
if (onSelectionLimitReachedListener != null) {
|
||||
onSelectionLimitReachedListener.onHardLimitReached(selectionLimit.getHardLimit());
|
||||
} else {
|
||||
GroupLimitDialog.showHardLimitMessage(requireContext());
|
||||
}
|
||||
GroupLimitDialog.showHardLimitMessage(requireContext());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -672,48 +476,44 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return UsernameUtil.fetchAciForUsername(contact.getNumber());
|
||||
return UsernameUtil.fetchUuidForUsername(requireContext(), contact.getNumber());
|
||||
}, uuid -> {
|
||||
loadingDialog.dismiss();
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
|
||||
if (allowed) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
});
|
||||
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber(), allowed -> {
|
||||
if (allowed) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
});
|
||||
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
markContactUnselected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||
@@ -739,118 +539,130 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (isMulti) {
|
||||
addChipForSelectedContact(selectedContact);
|
||||
}
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
contactChipViewModel.remove(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onSelectionChanged();
|
||||
}
|
||||
removeChipForContact(selectedContact);
|
||||
}
|
||||
|
||||
private void handleSelectedContactsChanged(@NonNull List<SelectedContacts.Model> selectedContacts) {
|
||||
contactChipAdapter.submitList(new MappingModelList(selectedContacts), this::smoothScrollChipsToEnd);
|
||||
|
||||
if (selectedContacts.isEmpty()) {
|
||||
setChipGroupVisibility(ConstraintSet.GONE);
|
||||
} else {
|
||||
setChipGroupVisibility(ConstraintSet.VISIBLE);
|
||||
private void removeChipForContact(@NonNull SelectedContact contact) {
|
||||
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) {
|
||||
View v = chipGroup.getChildAt(i);
|
||||
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
|
||||
chipGroup.removeView(v);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionWarningLimitReachedExactly()) {
|
||||
if (onSelectionLimitReachedListener != null) {
|
||||
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
|
||||
} else {
|
||||
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||
}
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
|
||||
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||
resolved -> contactChipViewModel.add(selectedContact));
|
||||
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||
resolved -> addChipForRecipient(resolved, selectedContact));
|
||||
}
|
||||
|
||||
private Unit onChipCloseIconClicked(SelectedContacts.Model model) {
|
||||
markContactUnselected(model.getSelectedContact());
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(model.getRecipient().getId()), model.getRecipient().getE164().orElse(null));
|
||||
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
|
||||
final ContactChip chip = new ContactChip(requireContext());
|
||||
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.VISIBLE);
|
||||
}
|
||||
|
||||
return Unit.INSTANCE;
|
||||
chip.setText(recipient.getShortDisplayName(requireContext()));
|
||||
chip.setContact(selectedContact);
|
||||
chip.setCloseIconVisible(true);
|
||||
chip.setOnCloseIconClickListener(view -> {
|
||||
markContactUnselected(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
|
||||
}
|
||||
});
|
||||
|
||||
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
|
||||
@Override
|
||||
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||
if (view == chip && transitionType == LayoutTransition.APPEARING) {
|
||||
chipGroup.getLayoutTransition().removeTransitionListener(this);
|
||||
registerChipRecipientObserver(chip, recipient.live());
|
||||
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
|
||||
}
|
||||
|
||||
private void addChip(@NonNull ContactChip chip) {
|
||||
chipGroup.addView(chip);
|
||||
updateGroupLimit(getChipCount());
|
||||
if (selectionWarningLimitReachedExactly()) {
|
||||
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
private int getChipCount() {
|
||||
int count = contactChipViewModel.getCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
||||
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
||||
if (count < 0) throw new AssertionError();
|
||||
return count;
|
||||
}
|
||||
|
||||
private void setChipGroupVisibility(int visibility) {
|
||||
if (!safeArguments().getBoolean(DISPLAY_CHIPS, requireActivity().getIntent().getBooleanExtra(DISPLAY_CHIPS, true))) {
|
||||
return;
|
||||
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
|
||||
if (recipient != null) {
|
||||
recipient.observe(getViewLifecycleOwner(), resolved -> {
|
||||
if (chip.isAttachedToWindow()) {
|
||||
chip.setAvatar(glideRequests, resolved, null);
|
||||
chip.setText(resolved.getShortDisplayName(chip.getContext()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AutoTransition transition = new AutoTransition();
|
||||
transition.setDuration(CHIP_GROUP_REVEAL_DURATION_MS);
|
||||
transition.excludeChildren(recyclerView, true);
|
||||
transition.excludeTarget(recyclerView, true);
|
||||
TransitionManager.beginDelayedTransition(constraintLayout, transition);
|
||||
private void setChipGroupVisibility(int visibility) {
|
||||
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(constraintLayout);
|
||||
constraintSet.setVisibility(R.id.chipRecycler, visibility);
|
||||
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility);
|
||||
constraintSet.applyTo(constraintLayout);
|
||||
}
|
||||
|
||||
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
|
||||
this.onContactSelectedListener = onContactSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener onRefreshListener) {
|
||||
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
|
||||
}
|
||||
|
||||
private void smoothScrollChipsToEnd() {
|
||||
int x = ViewUtil.isLtr(chipRecycler) ? chipRecycler.getWidth() : 0;
|
||||
chipRecycler.smoothScrollBy(x, 0);
|
||||
int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0;
|
||||
chipGroupScrollContainer.smoothScrollTo(x, 0);
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
/**
|
||||
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
|
||||
*/
|
||||
void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, @Nullable String number, @NonNull Consumer<Boolean> callback);
|
||||
|
||||
void onContactDeselected(@NonNull Optional<RecipientId> recipientId, @Nullable String number);
|
||||
|
||||
void onSelectionChanged();
|
||||
}
|
||||
|
||||
public interface OnSelectionLimitReachedListener {
|
||||
void onSuggestedLimitReached(int limit);
|
||||
|
||||
void onHardLimitReached(int limit);
|
||||
/** @return True if the contact is allowed to be selected, otherwise false. */
|
||||
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
}
|
||||
|
||||
public interface ListCallback {
|
||||
void onInvite();
|
||||
|
||||
void onNewGroup(boolean forceV1);
|
||||
}
|
||||
|
||||
public interface ScrollCallback {
|
||||
void onBeginScroll();
|
||||
}
|
||||
|
||||
public interface HeaderActionProvider {
|
||||
@NonNull HeaderAction getHeaderAction();
|
||||
}
|
||||
|
||||
public interface AbstractContactsCursorLoaderFactoryProvider {
|
||||
@NonNull AbstractContactsCursorLoader.Factory get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Parcelable;
|
||||
import android.view.View;
|
||||
@@ -151,7 +150,7 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
|
||||
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
|
||||
} else {
|
||||
// TODO [greyson] Navigation
|
||||
startActivity(MainActivity.clearTop(this));
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,11 +158,6 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
|
||||
}
|
||||
|
||||
private class ImportStateHandler extends Handler {
|
||||
|
||||
public ImportStateHandler() {
|
||||
super(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
switch (message.what) {
|
||||
|
||||
@@ -16,26 +16,25 @@ import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.qr.kitkat.ScanListener;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
|
||||
@@ -46,9 +45,9 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
implements Button.OnClickListener, ScanListener, DeviceLinkFragment.LinkClickedListener
|
||||
{
|
||||
|
||||
private static final String TAG = Log.tag(DeviceActivity.class);
|
||||
private static final String TAG = DeviceActivity.class.getSimpleName();
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private DeviceAddFragment deviceAddFragment;
|
||||
@@ -63,14 +62,9 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.device_activity);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
requireSupportActionBar().setTitle(R.string.AndroidManifest__linked_devices);
|
||||
|
||||
getSupportActionBar().setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24));
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__linked_devices);
|
||||
this.deviceAddFragment = new DeviceAddFragment();
|
||||
this.deviceListFragment = new DeviceListFragment();
|
||||
this.deviceLinkFragment = new DeviceLinkFragment();
|
||||
@@ -79,10 +73,20 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
this.deviceAddFragment.setScanListener(this);
|
||||
|
||||
if (getIntent().getBooleanExtra("add", false)) {
|
||||
initFragment(R.id.fragment_container, deviceAddFragment, dynamicLanguage.getCurrentLocale());
|
||||
initFragment(android.R.id.content, deviceAddFragment, dynamicLanguage.getCurrentLocale());
|
||||
} else {
|
||||
initFragment(R.id.fragment_container, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
||||
initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
||||
}
|
||||
|
||||
overridePendingTransition(R.anim.slide_from_end, R.anim.slide_to_start);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (isFinishing()) {
|
||||
overridePendingTransition(R.anim.slide_from_start, R.anim.slide_to_end);
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,9 +98,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: finish(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -110,7 +113,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
|
||||
.onAllGranted(() -> {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_container, deviceAddFragment)
|
||||
.replace(android.R.id.content, deviceAddFragment)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
})
|
||||
@@ -119,13 +122,13 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQrDataFound(@NonNull final String data) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
public void onQrDataFound(final String data) {
|
||||
Util.runOnMain(() -> {
|
||||
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
|
||||
Uri uri = Uri.parse(data);
|
||||
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
|
||||
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
|
||||
|
||||
@@ -135,21 +138,20 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.addToBackStack(null)
|
||||
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
|
||||
.replace(R.id.fragment_container, deviceLinkFragment)
|
||||
.replace(android.R.id.content, deviceLinkFragment)
|
||||
.commit();
|
||||
|
||||
} else {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
|
||||
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
|
||||
.replace(R.id.fragment_container, deviceLinkFragment)
|
||||
.replace(android.R.id.content, deviceLinkFragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
@@ -185,13 +187,13 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
return BAD_CODE;
|
||||
}
|
||||
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
|
||||
IdentityKeyPair aciIdentityKeyPair = SignalStore.account().getAciIdentityKey();
|
||||
IdentityKeyPair pniIdentityKeyPair = SignalStore.account().getPniIdentityKey();
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
|
||||
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext()));
|
||||
|
||||
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
|
||||
accountManager.addDevice(ephemeralId, publicKey, aciIdentityKeyPair, pniIdentityKeyPair, profileKey, verificationCode);
|
||||
TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false);
|
||||
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode);
|
||||
|
||||
return SUCCESS;
|
||||
} catch (NotFoundException e) {
|
||||
|
||||
@@ -14,35 +14,37 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import org.signal.qr.QrScannerView;
|
||||
import org.signal.qr.kitkat.ScanListener;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public class DeviceAddFragment extends LoggingFragment {
|
||||
|
||||
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
private ImageView devicesImage;
|
||||
private ScanListener scanListener;
|
||||
private ViewGroup container;
|
||||
private LinearLayout overlay;
|
||||
private ImageView devicesImage;
|
||||
private CameraView scannerView;
|
||||
private ScanningThread scanningThread;
|
||||
private ScanListener scanListener;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
|
||||
this.overlay = ViewUtil.findById(this.container, R.id.overlay);
|
||||
this.scannerView = ViewUtil.findById(this.container, R.id.scanner);
|
||||
this.devicesImage = ViewUtil.findById(this.container, R.id.devices);
|
||||
|
||||
QrScannerView scannerView = container.findViewById(R.id.scanner);
|
||||
this.devicesImage = container.findViewById(R.id.devices);
|
||||
ViewCompat.setTransitionName(devicesImage, "devices");
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
this.overlay.setOrientation(LinearLayout.HORIZONTAL);
|
||||
} else {
|
||||
this.overlay.setOrientation(LinearLayout.VERTICAL);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
@TargetApi(21)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||
int oldLeft, int oldTop, int oldRight, int oldBottom)
|
||||
@@ -57,30 +59,54 @@ public class DeviceAddFragment extends LoggingFragment {
|
||||
});
|
||||
}
|
||||
|
||||
scannerView.start(getViewLifecycleOwner(), FeatureFlags.useQrLegacyScan());
|
||||
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
|
||||
Disposable qrDisposable = scannerView
|
||||
.getQrData()
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(qrData -> {
|
||||
if (scanListener != null) {
|
||||
scanListener.onQrDataFound(qrData);
|
||||
}
|
||||
});
|
||||
|
||||
lifecycleDisposable.add(qrDisposable);
|
||||
|
||||
return container;
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
this.scanningThread = new ScanningThread();
|
||||
this.scanningThread.setScanListener(scanListener);
|
||||
this.scannerView.onResume();
|
||||
this.scannerView.setPreviewCallback(scanningThread);
|
||||
this.scanningThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
this.scannerView.onPause();
|
||||
this.scanningThread.stopScanning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
|
||||
this.scannerView.onPause();
|
||||
|
||||
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
overlay.setOrientation(LinearLayout.HORIZONTAL);
|
||||
} else {
|
||||
overlay.setOrientation(LinearLayout.VERTICAL);
|
||||
}
|
||||
|
||||
this.scannerView.onResume();
|
||||
this.scannerView.setPreviewCallback(scanningThread);
|
||||
}
|
||||
|
||||
|
||||
public ImageView getDevicesImage() {
|
||||
return devicesImage;
|
||||
}
|
||||
|
||||
public void setScanListener(ScanListener scanListener) {
|
||||
this.scanListener = scanListener;
|
||||
|
||||
if (this.scanningThread != null) {
|
||||
this.scanningThread.setScanListener(scanListener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class DeviceLinkFragment extends Fragment implements View.OnClickListener {
|
||||
@@ -22,7 +21,6 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = (LinearLayout) inflater.inflate(R.layout.device_link_fragment, container, false);
|
||||
this.container.findViewById(R.id.link_device).setOnClickListener(this);
|
||||
ViewCompat.setTransitionName(container.findViewById(R.id.devices), "devices");
|
||||
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
container.setOrientation(LinearLayout.HORIZONTAL);
|
||||
@@ -34,7 +32,7 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
|
||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
container.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -20,14 +21,14 @@ import androidx.fragment.app.ListFragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
||||
@@ -40,7 +41,7 @@ public class DeviceListFragment extends ListFragment
|
||||
ListView.OnItemClickListener, Button.OnClickListener
|
||||
{
|
||||
|
||||
private static final String TAG = Log.tag(DeviceListFragment.class);
|
||||
private static final String TAG = DeviceListFragment.class.getSimpleName();
|
||||
|
||||
private SignalServiceAccountManager accountManager;
|
||||
private Locale locale;
|
||||
@@ -52,12 +53,12 @@ public class DeviceListFragment extends ListFragment
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
this.locale = (Locale) requireArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
}
|
||||
|
||||
@@ -67,7 +68,7 @@ public class DeviceListFragment extends ListFragment
|
||||
|
||||
this.empty = view.findViewById(R.id.empty);
|
||||
this.progressContainer = view.findViewById(R.id.progress_container);
|
||||
this.addDeviceButton = view.findViewById(R.id.add_device);
|
||||
this.addDeviceButton = ViewUtil.findById(view, R.id.add_device);
|
||||
this.addDeviceButton.setOnClickListener(this);
|
||||
|
||||
return view;
|
||||
@@ -121,22 +122,42 @@ public class DeviceListFragment extends ListFragment
|
||||
final String deviceName = ((DeviceListItem)view).getDeviceName();
|
||||
final long deviceId = ((DeviceListItem)view).getDeviceId();
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
|
||||
builder.setTitle(getString(R.string.DeviceListActivity_unlink_s, deviceName));
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
|
||||
builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> handleDisconnectDevice(deviceId));
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
handleDisconnectDevice(deviceId);
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void handleLoaderFailed() {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setMessage(R.string.DeviceListActivity_network_connection_failed);
|
||||
builder.setPositiveButton(R.string.DeviceListActivity_try_again,
|
||||
(dialog, which) -> getLoaderManager().restartLoader(0, null, DeviceListFragment.this));
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> requireActivity().onBackPressed());
|
||||
builder.setOnCancelListener(dialog -> requireActivity().onBackPressed());
|
||||
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
DeviceListFragment.this.getActivity().onBackPressed();
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
DeviceListFragment.this.getActivity().onBackPressed();
|
||||
}
|
||||
});
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import android.view.Window;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(DeviceProvisioningActivity.class);
|
||||
private static final String TAG = DeviceProvisioningActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
|
||||
|
||||
public class ExpirationDialog extends AlertDialog {
|
||||
|
||||
protected ExpirationDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
protected ExpirationDialog(Context context, int theme) {
|
||||
super(context, theme);
|
||||
}
|
||||
|
||||
protected ExpirationDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
|
||||
super(context, cancelable, cancelListener);
|
||||
}
|
||||
|
||||
public static void show(final Context context,
|
||||
final int currentExpiration,
|
||||
final @NonNull OnClickListener listener)
|
||||
{
|
||||
final View view = createNumberPickerView(context, currentExpiration);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
|
||||
listener.onClick(getExpirationTimes(context, currentExpiration)[selected]);
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static View createNumberPickerView(final Context context, final int currentExpiration) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
final View view = inflater.inflate(R.layout.expiration_dialog, null);
|
||||
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
|
||||
final TextView textView = view.findViewById(R.id.expiration_details);
|
||||
final int[] expirationTimes = getExpirationTimes(context, currentExpiration);
|
||||
final String[] expirationDisplayValues = new String[expirationTimes.length];
|
||||
|
||||
int selectedIndex = expirationTimes.length - 1;
|
||||
|
||||
for (int i=0;i<expirationTimes.length;i++) {
|
||||
expirationDisplayValues[i] = ExpirationUtil.getExpirationDisplayValue(context, expirationTimes[i]);
|
||||
|
||||
if ((currentExpiration >= expirationTimes[i]) &&
|
||||
(i == expirationTimes.length -1 || currentExpiration < expirationTimes[i+1])) {
|
||||
selectedIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
numberPickerView.setDisplayedValues(expirationDisplayValues);
|
||||
numberPickerView.setMinValue(0);
|
||||
numberPickerView.setMaxValue(expirationTimes.length-1);
|
||||
|
||||
NumberPickerView.OnValueChangeListener listener = (picker, oldVal, newVal) -> {
|
||||
if (newVal == 0) {
|
||||
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
|
||||
} else {
|
||||
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
|
||||
}
|
||||
};
|
||||
|
||||
numberPickerView.setOnValueChangedListener(listener);
|
||||
numberPickerView.setValue(selectedIndex);
|
||||
listener.onValueChange(numberPickerView, selectedIndex, selectedIndex);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private static int[] getExpirationTimes(Context context, int currentExpiration) {
|
||||
int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
|
||||
int location = Arrays.binarySearch(expirationTimes, currentExpiration);
|
||||
if (location < 0) {
|
||||
int[] temp = Arrays.copyOf(expirationTimes, expirationTimes.length + 1);
|
||||
temp[temp.length - 1] = currentExpiration;
|
||||
Arrays.sort(temp);
|
||||
expirationTimes = temp;
|
||||
}
|
||||
|
||||
return expirationTimes;
|
||||
}
|
||||
|
||||
public interface OnClickListener {
|
||||
public void onClick(int expirationTime);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,7 +35,6 @@ public final class GroupMembersDialog {
|
||||
.show();
|
||||
|
||||
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
||||
memberListView.initializeAdapter(fragmentActivity);
|
||||
|
||||
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
|
||||
LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers();
|
||||
|
||||
@@ -3,7 +3,9 @@ package org.thoughtcrime.securesms;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
@@ -12,20 +14,19 @@ import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AnimRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -33,25 +34,25 @@ import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
private ViewGroup smsSendFrame;
|
||||
private Button smsSendButton;
|
||||
private Animation slideInAnimation;
|
||||
private Animation slideOutAnimation;
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme();
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
private ViewGroup smsSendFrame;
|
||||
private Button smsSendButton;
|
||||
private Animation slideInAnimation;
|
||||
private Animation slideOutAnimation;
|
||||
private DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme();
|
||||
private Toolbar primaryToolbar;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
@@ -79,7 +80,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
private void initializeAppBar() {
|
||||
final Toolbar primaryToolbar = findViewById(R.id.toolbar);
|
||||
primaryToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(primaryToolbar);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
@@ -92,40 +93,26 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
slideInAnimation = loadAnimation(R.anim.slide_from_bottom);
|
||||
slideOutAnimation = loadAnimation(R.anim.slide_to_bottom);
|
||||
|
||||
View shareButton = findViewById(R.id.share_button);
|
||||
TextView shareText = findViewById(R.id.share_text);
|
||||
View smsButton = findViewById(R.id.sms_button);
|
||||
Button smsCancelButton = findViewById(R.id.cancel_sms_button);
|
||||
ContactFilterView contactFilter = findViewById(R.id.contact_filter_edit_text);
|
||||
View shareButton = ViewUtil.findById(this, R.id.share_button);
|
||||
View smsButton = ViewUtil.findById(this, R.id.sms_button);
|
||||
Button smsCancelButton = ViewUtil.findById(this, R.id.cancel_sms_button);
|
||||
ContactFilterToolbar contactFilter = ViewUtil.findById(this, R.id.contact_filter);
|
||||
|
||||
inviteText = findViewById(R.id.invite_text);
|
||||
smsSendFrame = findViewById(R.id.sms_send_frame);
|
||||
smsSendButton = findViewById(R.id.send_sms_button);
|
||||
inviteText = ViewUtil.findById(this, R.id.invite_text);
|
||||
smsSendFrame = ViewUtil.findById(this, R.id.sms_send_frame);
|
||||
smsSendButton = ViewUtil.findById(this, R.id.send_sms_button);
|
||||
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
|
||||
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||
inviteText.addTextChangedListener(new AfterTextChanged(editable -> {
|
||||
boolean isEnabled = editable.length() > 0;
|
||||
smsButton.setEnabled(isEnabled);
|
||||
shareButton.setEnabled(isEnabled);
|
||||
smsButton.animate().alpha(isEnabled ? 1f : 0.5f);
|
||||
shareButton.animate().alpha(isEnabled ? 1f : 0.5f);
|
||||
}));
|
||||
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
smsButton.setOnClickListener(new SmsClickListener());
|
||||
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
|
||||
if (Util.isDefaultSmsProvider(this)) {
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
smsButton.setOnClickListener(new SmsClickListener());
|
||||
} else {
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
contactFilter.setNavigationIcon(R.drawable.ic_search_conversation_24);
|
||||
}
|
||||
|
||||
private Animation loadAnimation(@AnimRes int animResId) {
|
||||
@@ -135,20 +122,16 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||
callback.accept(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
|
||||
private void sendSmsInvites() {
|
||||
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
@@ -157,7 +140,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
private void updateSmsButtonText(int count) {
|
||||
smsSendButton.setText(getResources().getString(R.string.InviteActivity_send_sms, count));
|
||||
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
|
||||
count,
|
||||
count));
|
||||
smsSendButton.setEnabled(count > 0);
|
||||
}
|
||||
|
||||
@@ -169,21 +154,43 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
}
|
||||
|
||||
@Override public boolean onSupportNavigateUp() {
|
||||
if (smsSendFrame.getVisibility() == View.VISIBLE) {
|
||||
cancelSmsSelection();
|
||||
return false;
|
||||
} else {
|
||||
return super.onSupportNavigateUp();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelSmsSelection() {
|
||||
setPrimaryColorsToolbarNormal();
|
||||
contactsFragment.reset();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
||||
}
|
||||
|
||||
private void setPrimaryColorsToolbarNormal() {
|
||||
primaryToolbar.setBackgroundColor(0);
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(null);
|
||||
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_primary));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
WindowUtil.setStatusBarColor(getWindow(), ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
|
||||
getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor));
|
||||
WindowUtil.setLightStatusBarFromTheme(this);
|
||||
}
|
||||
|
||||
WindowUtil.setLightNavigationBarFromTheme(this);
|
||||
}
|
||||
|
||||
private void setPrimaryColorsToolbarForSms() {
|
||||
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(ContextCompat.getColor(this, R.color.signal_text_toolbar_subtitle), PorterDuff.Mode.SRC_IN);
|
||||
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_toolbar_title));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
WindowUtil.setStatusBarColor(getWindow(), ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
WindowUtil.clearLightStatusBar(getWindow());
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
WindowUtil.clearLightNavigationBar(getWindow());
|
||||
}
|
||||
}
|
||||
|
||||
private class ShareClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -202,6 +209,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
private class SmsClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
setPrimaryColorsToolbarForSms();
|
||||
ViewUtil.animateIn(smsSendFrame, slideInAnimation);
|
||||
}
|
||||
}
|
||||
@@ -251,12 +259,12 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
for (SelectedContact contact : contacts) {
|
||||
RecipientId recipientId = contact.getOrCreateRecipientId(context);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
|
||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
|
||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null);
|
||||
|
||||
if (recipient.getContactUri() != null) {
|
||||
SignalDatabase.recipients().setHasSentInvite(recipient.getId());
|
||||
DatabaseFactory.getRecipientDatabase(context).setHasSentInvite(recipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -16,12 +15,6 @@ public abstract class LoggingFragment extends Fragment {
|
||||
|
||||
private static final String TAG = Log.tag(LoggingFragment.class);
|
||||
|
||||
public LoggingFragment() { }
|
||||
|
||||
public LoggingFragment(@LayoutRes int contentLayoutId) {
|
||||
super(contentLayoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
logEvent("onCreate()");
|
||||
|
||||
@@ -1,87 +1,43 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.SplashScreenUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
|
||||
@Trace
|
||||
public class MainActivity extends PassphraseRequiredActivity {
|
||||
|
||||
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
|
||||
private VoiceNoteMediaController mediaController;
|
||||
private ConversationListTabsViewModel conversationListTabsViewModel;
|
||||
|
||||
public static @NonNull Intent clearTop(@NonNull Context context) {
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK |
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
mediaController = new VoiceNoteMediaController(this);
|
||||
|
||||
ConversationListTabRepository repository = new ConversationListTabRepository();
|
||||
ConversationListTabsViewModel.Factory factory = new ConversationListTabsViewModel.Factory(repository);
|
||||
navigator.onCreate(savedInstanceState);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
handleProxyInIntent(getIntent());
|
||||
handleSignalMeIntent(getIntent());
|
||||
|
||||
CachedInflater.from(this).clear();
|
||||
|
||||
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
|
||||
updateTabVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getIntent() {
|
||||
return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK |
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleGroupLinkInIntent(intent);
|
||||
handleProxyInIntent(intent);
|
||||
handleSignalMeIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,17 +50,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||
OldDeviceTransferLockedDialog.show(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
updateTabVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
SplashScreenUtil.setSplashScreenThemeIfNecessary(this, SignalStore.settings().getTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,17 +67,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTabVisibility() {
|
||||
if (Stories.isFeatureEnabled()) {
|
||||
findViewById(R.id.conversation_list_tabs).setVisibility(View.VISIBLE);
|
||||
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorSurface2));
|
||||
} else {
|
||||
findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE);
|
||||
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorBackground));
|
||||
conversationListTabsViewModel.onChatsSelected();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull MainNavigator getNavigator() {
|
||||
return navigator;
|
||||
}
|
||||
@@ -143,23 +77,4 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleProxyInIntent(Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSignalMeIntent(Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
CommunicationActions.handlePotentialSignalMeUrl(this, data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
|
||||
return mediaController;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -31,6 +34,16 @@ public class MainNavigator {
|
||||
return ((MainActivity) activity).getNavigator();
|
||||
}
|
||||
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_container, ConversationListFragment.newInstance())
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the back pressed was handled in our own custom way, false if it should be given
|
||||
* to the system to do the default behavior.
|
||||
@@ -56,7 +69,16 @@ public class MainNavigator {
|
||||
}
|
||||
|
||||
public void goToAppSettings() {
|
||||
activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES);
|
||||
Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
|
||||
activity.startActivityForResult(intent, REQUEST_CONFIG_CHANGES);
|
||||
}
|
||||
|
||||
public void goToArchiveList() {
|
||||
getFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(R.id.fragment_container, ConversationListArchiveFragment.newInstance())
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void goToGroupCreation() {
|
||||
|
||||
@@ -21,11 +21,13 @@ import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -55,10 +57,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.animation.DepthPageTransformer;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||
@@ -71,10 +69,10 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
@@ -90,11 +88,10 @@ import java.util.Objects;
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
MediaPreviewFragment.Events,
|
||||
VoiceNoteMediaControllerOwner
|
||||
MediaPreviewFragment.Events
|
||||
{
|
||||
|
||||
private final static String TAG = Log.tag(MediaPreviewActivity.class);
|
||||
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
|
||||
|
||||
private static final int NOT_IN_A_THREAD = -2;
|
||||
|
||||
@@ -106,7 +103,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media";
|
||||
public static final String SHOW_THREAD_EXTRA = "show_thread";
|
||||
public static final String SORTING_EXTRA = "sorting";
|
||||
public static final String IS_VIDEO_GIF = "is_video_gif";
|
||||
|
||||
private ViewPager mediaPager;
|
||||
private View detailsContainer;
|
||||
@@ -119,7 +115,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
private String initialMediaType;
|
||||
private long initialMediaSize;
|
||||
private String initialCaption;
|
||||
private boolean initialMediaIsVideoGif;
|
||||
private boolean leftIsRecent;
|
||||
private MediaPreviewViewModel viewModel;
|
||||
private ViewPagerListener viewPagerListener;
|
||||
@@ -131,8 +126,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
private MediaDatabase.Sorting sorting;
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
|
||||
private @Nullable Cursor cursor = null;
|
||||
|
||||
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
|
||||
@@ -146,7 +139,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||
intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif());
|
||||
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
|
||||
return intent;
|
||||
}
|
||||
@@ -165,7 +157,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
voiceNoteMediaController = new VoiceNoteMediaController(this);
|
||||
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
|
||||
|
||||
fullscreenHelper = new FullscreenHelper(this);
|
||||
@@ -177,7 +168,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
initializeObservers();
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
@@ -210,25 +200,23 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
else from = "";
|
||||
|
||||
if (showThread) {
|
||||
String titleText = null;
|
||||
String to = null;
|
||||
Recipient threadRecipient = mediaItem.threadRecipient;
|
||||
|
||||
if (threadRecipient != null) {
|
||||
if (mediaItem.outgoing) {
|
||||
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
||||
if (threadRecipient.isSelf()) {
|
||||
titleText = getString(R.string.note_to_self);
|
||||
from = getString(R.string.note_to_self);
|
||||
} else {
|
||||
titleText = getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(this));
|
||||
to = threadRecipient.getDisplayName(this);
|
||||
}
|
||||
} else {
|
||||
if (threadRecipient.isGroup()) {
|
||||
titleText = getString(R.string.MediaPreviewActivity_s_to_s, from, threadRecipient.getDisplayName(this));
|
||||
} else {
|
||||
titleText = getString(R.string.MediaPreviewActivity_s_to_you, from);
|
||||
}
|
||||
to = getString(R.string.MediaPreviewActivity_you);
|
||||
}
|
||||
}
|
||||
return titleText != null ? titleText : from;
|
||||
|
||||
return to != null ? getString(R.string.MediaPreviewActivity_s_to_s, from, to)
|
||||
: from;
|
||||
} else {
|
||||
return from;
|
||||
}
|
||||
@@ -295,7 +283,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
anchorMarginsToBottomInsets(detailsContainer);
|
||||
|
||||
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), findViewById(R.id.toolbar));
|
||||
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||
|
||||
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
||||
}
|
||||
@@ -308,13 +296,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false);
|
||||
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)];
|
||||
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
initialMediaIsVideoGif = intent.getBooleanExtra(IS_VIDEO_GIF, false);
|
||||
restartItem = -1;
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
restartItem = -1;
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
@@ -367,7 +354,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
if (isMediaInDb()) {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
} else {
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize, initialMediaIsVideoGif));
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize));
|
||||
|
||||
if (initialCaption != null) {
|
||||
detailsContainer.setVisibility(View.VISIBLE);
|
||||
@@ -395,12 +382,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
MultiselectForwardFragmentArgs.create(
|
||||
this,
|
||||
mediaItem.uri,
|
||||
mediaItem.type,
|
||||
args -> MultiselectForwardFragment.showBottomSheet(getSupportFragmentManager(), args)
|
||||
);
|
||||
Intent composeIntent = new Intent(this, ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_STREAM, mediaItem.uri);
|
||||
composeIntent.setType(mediaItem.type);
|
||||
startActivity(composeIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +529,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
public static boolean isContentTypeSupported(final String contentType) {
|
||||
return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType);
|
||||
return contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -564,33 +549,40 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
cursor = Objects.requireNonNull(data.first);
|
||||
|
||||
viewModel.setCursor(this, cursor, leftIsRecent);
|
||||
|
||||
int mediaPosition = Objects.requireNonNull(data.second);
|
||||
|
||||
CursorPagerAdapter oldAdapter = (CursorPagerAdapter) mediaPager.getAdapter();
|
||||
if (oldAdapter == null) {
|
||||
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(), this, cursor, mediaPosition, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
} else {
|
||||
oldAdapter.setCursor(cursor, mediaPosition);
|
||||
oldAdapter.setActive(true);
|
||||
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(),this, cursor, mediaPosition, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
|
||||
viewModel.setCursor(this, cursor, leftIsRecent);
|
||||
|
||||
int item = restartItem >= 0 ? restartItem : mediaPosition;
|
||||
mediaPager.setCurrentItem(item);
|
||||
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
}
|
||||
|
||||
if (oldAdapter == null || restartItem >= 0) {
|
||||
int item = restartItem >= 0 ? restartItem : mediaPosition;
|
||||
mediaPager.setCurrentItem(item);
|
||||
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
cursor.registerContentObserver(new ContentObserver(new Handler(getMainLooper())) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
onMediaChange();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mediaNotAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
private void onMediaChange() {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
adapter.checkMedia(mediaPager.getCurrentItem());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
|
||||
|
||||
@@ -608,15 +600,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaReady() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
|
||||
return voiceNoteMediaController;
|
||||
}
|
||||
|
||||
private class ViewPagerListener extends ExtendedOnPageChangedListener {
|
||||
|
||||
@Override
|
||||
@@ -627,10 +610,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (adapter != null) {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item != null && item.recipient != null) {
|
||||
item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
|
||||
}
|
||||
|
||||
if (item.recipient != null) item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
|
||||
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
|
||||
initializeActionBar();
|
||||
}
|
||||
@@ -643,9 +623,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (adapter != null) {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item != null && item.recipient != null) {
|
||||
item.recipient.live().removeObservers(MediaPreviewActivity.this);
|
||||
}
|
||||
if (item.recipient != null) item.recipient.live().removeObservers(MediaPreviewActivity.this);
|
||||
|
||||
adapter.pause(position);
|
||||
}
|
||||
@@ -654,24 +632,21 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
private static class SingleItemPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
|
||||
|
||||
private final Uri uri;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
private final boolean isVideoGif;
|
||||
private final Uri uri;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
|
||||
private MediaPreviewFragment mediaPreviewFragment;
|
||||
|
||||
SingleItemPagerAdapter(@NonNull FragmentManager fragmentManager,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String mediaType,
|
||||
long size,
|
||||
boolean isVideoGif)
|
||||
long size)
|
||||
{
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.uri = uri;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
this.isVideoGif = isVideoGif;
|
||||
this.uri = uri;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -682,7 +657,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true, isVideoGif);
|
||||
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true);
|
||||
return mediaPreviewFragment;
|
||||
}
|
||||
|
||||
@@ -695,7 +670,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable MediaItem getMediaItemFor(int position) {
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
return new MediaItem(null, null, null, uri, mediaType, -1, true);
|
||||
}
|
||||
|
||||
@@ -718,6 +693,11 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
public boolean hasFragmentFor(int position) {
|
||||
return mediaPreviewFragment != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMedia(int currentItem) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
|
||||
@@ -741,10 +721,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
private final Map<Integer, MediaPreviewFragment> mediaFragments = new HashMap<>();
|
||||
|
||||
private final Context context;
|
||||
private final Cursor cursor;
|
||||
private final boolean leftIsRecent;
|
||||
|
||||
private boolean active;
|
||||
private Cursor cursor;
|
||||
private int autoPlayPosition;
|
||||
|
||||
CursorPagerAdapter(@NonNull FragmentManager fragmentManager,
|
||||
@@ -765,11 +745,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setCursor(@NonNull Cursor cursor, int autoPlayPosition) {
|
||||
this.cursor = cursor;
|
||||
this.autoPlayPosition = autoPlayPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (!active) return 0;
|
||||
@@ -806,15 +781,8 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
|
||||
public @Nullable MediaItem getMediaItemFor(int position) {
|
||||
int cursorPosition = getCursorPosition(position);
|
||||
|
||||
if (cursor.isClosed() || cursorPosition < 0) {
|
||||
Log.w(TAG, "Invalid cursor state! Closed: " + cursor.isClosed() + " Position: " + cursorPosition);
|
||||
return null;
|
||||
}
|
||||
|
||||
cursor.moveToPosition(cursorPosition);
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
cursor.moveToPosition(getCursorPosition(position));
|
||||
|
||||
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
@@ -848,6 +816,14 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
return mediaFragments.containsKey(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMedia(int position) {
|
||||
MediaPreviewFragment fragment = mediaFragments.get(position);
|
||||
if (fragment != null) {
|
||||
fragment.checkMediaStillAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
private int getCursorPosition(int position) {
|
||||
if (leftIsRecent) return position;
|
||||
else return cursor.getCount() - 1 - position;
|
||||
@@ -882,9 +858,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
interface MediaItemAdapter {
|
||||
@Nullable MediaItem getMediaItemFor(int position);
|
||||
MediaItem getMediaItemFor(int position);
|
||||
void pause(int position);
|
||||
@Nullable View getPlaybackControls(int position);
|
||||
boolean hasFragmentFor(int position);
|
||||
void checkMedia(int currentItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MuteDialog extends AlertDialog {
|
||||
@@ -30,21 +29,24 @@ public class MuteDialog extends AlertDialog {
|
||||
}
|
||||
|
||||
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
||||
builder.setItems(R.array.mute_durations, (dialog, which) -> {
|
||||
final long muteUntil;
|
||||
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, final int which) {
|
||||
final long muteUntil;
|
||||
|
||||
switch (which) {
|
||||
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
|
||||
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
|
||||
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
|
||||
case 4: muteUntil = Long.MAX_VALUE; break;
|
||||
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
switch (which) {
|
||||
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2); break;
|
||||
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
|
||||
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
|
||||
case 4: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365); break;
|
||||
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
}
|
||||
|
||||
listener.onMuted(muteUntil);
|
||||
}
|
||||
|
||||
listener.onMuted(muteUntil);
|
||||
});
|
||||
|
||||
if (cancelListener != null) {
|
||||
|
||||
@@ -21,24 +21,22 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Activity container for starting a new conversation.
|
||||
@@ -51,24 +49,23 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
{
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(NewConversationActivity.class);
|
||||
private static final String TAG = NewConversationActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
super.onCreate(bundle, ready);
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
|
||||
if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(getApplication())) {
|
||||
if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
|
||||
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
|
||||
|
||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||
@@ -76,10 +73,10 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Recipient resolved = Recipient.external(this, number);
|
||||
|
||||
if (!resolved.isRegistered() || !resolved.hasServiceId()) {
|
||||
if (!resolved.isRegistered() || !resolved.hasUuid()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
ContactDiscovery.refresh(this, resolved, false);
|
||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||
resolved = Recipient.resolved(resolved.getId());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
|
||||
@@ -96,15 +93,11 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
}
|
||||
|
||||
callback.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
long existingThread = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread)
|
||||
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
|
||||
.withDataUri(getIntent().getData())
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import androidx.annotation.ColorInt
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* BitmapTransformation which overlays the given bitmap with the given color.
|
||||
*/
|
||||
class OverlayTransformation(
|
||||
@ColorInt private val color: Int
|
||||
) : BitmapTransformation() {
|
||||
|
||||
private val id = "${OverlayTransformation::class.java.name}$color"
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(id.toByteArray(CHARSET))
|
||||
}
|
||||
|
||||
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
|
||||
val outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(outBitmap)
|
||||
|
||||
canvas.drawBitmap(toTransform, 0f, 0f, null)
|
||||
canvas.drawColor(color)
|
||||
|
||||
return outBitmap
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return (other as? OverlayTransformation)?.color == color
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
*/
|
||||
public abstract class PassphraseActivity extends BaseActivity {
|
||||
|
||||
private static final String TAG = Log.tag(PassphraseActivity.class);
|
||||
private static final String TAG = PassphraseActivity.class.getSimpleName();
|
||||
|
||||
private KeyCachingService keyCachingService;
|
||||
private MasterSecret masterSecret;
|
||||
|
||||
@@ -19,9 +19,9 @@ package org.thoughtcrime.securesms;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
|
||||
/**
|
||||
@@ -61,8 +61,7 @@ public class PassphraseCreateActivity extends PassphraseActivity {
|
||||
passphrase);
|
||||
|
||||
MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary();
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary();
|
||||
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
|
||||
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -35,6 +36,7 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.BounceInterpolator;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
@@ -44,13 +46,10 @@ import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricManager.Authenticators;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import androidx.core.os.CancellationSignal;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
@@ -58,10 +57,8 @@ import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
/**
|
||||
@@ -71,12 +68,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
*/
|
||||
public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
|
||||
private static final String TAG = Log.tag(PassphrasePromptActivity.class);
|
||||
private static final int BIOMETRIC_AUTHENTICATORS = Authenticators.BIOMETRIC_STRONG | Authenticators.BIOMETRIC_WEAK;
|
||||
private static final int ALLOWED_AUTHENTICATORS = BIOMETRIC_AUTHENTICATORS | Authenticators.DEVICE_CREDENTIAL;
|
||||
private static final short AUTHENTICATE_REQUEST_CODE = 1007;
|
||||
private static final String BUNDLE_ALREADY_SHOWN = "bundle_already_shown";
|
||||
public static final String FROM_FOREGROUND = "from_foreground";
|
||||
private static final String TAG = PassphrasePromptActivity.class.getSimpleName();
|
||||
|
||||
private DynamicIntroTheme dynamicTheme = new DynamicIntroTheme();
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
@@ -90,37 +82,24 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
private ImageButton hideButton;
|
||||
private AnimatingToggle visibilityToggle;
|
||||
|
||||
private BiometricManager biometricManager;
|
||||
private BiometricPrompt biometricPrompt;
|
||||
private BiometricPrompt.PromptInfo biometricPromptInfo;
|
||||
private FingerprintManagerCompat fingerprintManager;
|
||||
private CancellationSignal fingerprintCancellationSignal;
|
||||
private FingerprintListener fingerprintListener;
|
||||
|
||||
private boolean authenticated;
|
||||
private boolean hadFailure;
|
||||
private boolean alreadyShown;
|
||||
|
||||
private final Runnable resumeScreenLockRunnable = () -> {
|
||||
resumeScreenLock(!alreadyShown);
|
||||
alreadyShown = true;
|
||||
};
|
||||
private boolean failure;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Log.i(TAG, "onCreate()");
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.prompt_passphrase_activity);
|
||||
initializeResources();
|
||||
|
||||
alreadyShown = (savedInstanceState != null && savedInstanceState.getBoolean(BUNDLE_ALREADY_SHOWN)) ||
|
||||
getIntent().getBooleanExtra(FROM_FOREGROUND, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean(BUNDLE_ALREADY_SHOWN, alreadyShown);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -131,21 +110,20 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
|
||||
setLockTypeVisibility();
|
||||
|
||||
if (TextSecurePreferences.isScreenLockEnabled(this) && !authenticated && !hadFailure) {
|
||||
ThreadUtil.postToMain(resumeScreenLockRunnable);
|
||||
if (TextSecurePreferences.isScreenLockEnabled(this) && !authenticated && !failure) {
|
||||
resumeScreenLock();
|
||||
}
|
||||
|
||||
hadFailure = false;
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN);
|
||||
failure = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
ThreadUtil.cancelRunnableOnMain(resumeScreenLockRunnable);
|
||||
biometricPrompt.cancelAuthentication();
|
||||
|
||||
if (TextSecurePreferences.isScreenLockEnabled(this)) {
|
||||
pauseScreenLock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,7 +137,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
inflater.inflate(R.menu.passphrase_prompt, menu);
|
||||
inflater.inflate(R.menu.log_submit, menu);
|
||||
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
@@ -168,28 +146,23 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.menu_submit_debug_logs) {
|
||||
handleLogSubmit();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_contact_support) {
|
||||
sendEmailToSupport();
|
||||
return true;
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_submit_debug_logs: handleLogSubmit(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@SuppressLint("MissingSuperCall") // no fragments to dispatch to
|
||||
public void onActivityResult(int requestCode, int resultcode, Intent data) {
|
||||
if (requestCode != 1) return;
|
||||
|
||||
if (requestCode != AUTHENTICATE_REQUEST_CODE) return;
|
||||
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (resultcode == RESULT_OK) {
|
||||
handleAuthenticated();
|
||||
} else {
|
||||
Log.w(TAG, "Authentication failed");
|
||||
hadFailure = true;
|
||||
failure = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,20 +213,16 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
ImageButton okButton = findViewById(R.id.ok_button);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
showButton = findViewById(R.id.passphrase_visibility);
|
||||
hideButton = findViewById(R.id.passphrase_visibility_off);
|
||||
visibilityToggle = findViewById(R.id.button_toggle);
|
||||
passphraseText = findViewById(R.id.passphrase_edit);
|
||||
passphraseAuthContainer = findViewById(R.id.password_auth_container);
|
||||
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
|
||||
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
|
||||
biometricManager = BiometricManager.from(this);
|
||||
biometricPrompt = new BiometricPrompt(this, new BiometricAuthenticationListener());
|
||||
biometricPromptInfo = new BiometricPrompt.PromptInfo
|
||||
.Builder()
|
||||
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
|
||||
.setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal))
|
||||
.build();
|
||||
showButton = findViewById(R.id.passphrase_visibility);
|
||||
hideButton = findViewById(R.id.passphrase_visibility_off);
|
||||
visibilityToggle = findViewById(R.id.button_toggle);
|
||||
passphraseText = findViewById(R.id.passphrase_edit);
|
||||
passphraseAuthContainer = findViewById(R.id.password_auth_container);
|
||||
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
|
||||
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
|
||||
fingerprintManager = FingerprintManagerCompat.from(this);
|
||||
fingerprintCancellationSignal = new CancellationSignal();
|
||||
fingerprintListener = new FingerprintListener();
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setTitle("");
|
||||
@@ -273,15 +242,20 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||
|
||||
lockScreenButton.setOnClickListener(v -> resumeScreenLock(true));
|
||||
lockScreenButton.setOnClickListener(v -> resumeScreenLock());
|
||||
}
|
||||
|
||||
private void setLockTypeVisibility() {
|
||||
if (TextSecurePreferences.isScreenLockEnabled(this)) {
|
||||
passphraseAuthContainer.setVisibility(View.GONE);
|
||||
fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE
|
||||
: View.GONE);
|
||||
lockScreenButton.setVisibility(View.VISIBLE);
|
||||
|
||||
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
|
||||
fingerprintPrompt.setVisibility(View.VISIBLE);
|
||||
lockScreenButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
fingerprintPrompt.setVisibility(View.GONE);
|
||||
lockScreenButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
passphraseAuthContainer.setVisibility(View.VISIBLE);
|
||||
fingerprintPrompt.setVisibility(View.GONE);
|
||||
@@ -289,7 +263,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void resumeScreenLock(boolean force) {
|
||||
private void resumeScreenLock() {
|
||||
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||
|
||||
assert keyguardManager != null;
|
||||
@@ -300,36 +274,24 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT != 29 && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
if (force) {
|
||||
Log.i(TAG, "Listening for biometric authentication...");
|
||||
biometricPrompt.authenticate(biometricPromptInfo);
|
||||
} else {
|
||||
Log.i(TAG, "Skipping show system biometric dialog unless forced");
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (force) {
|
||||
Log.i(TAG, "firing intent...");
|
||||
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
|
||||
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE);
|
||||
} else {
|
||||
Log.i(TAG, "Skipping firing intent unless forced");
|
||||
}
|
||||
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
|
||||
Log.i(TAG, "Listening for fingerprints...");
|
||||
fingerprintCancellationSignal = new CancellationSignal();
|
||||
fingerprintManager.authenticate(null, 0, fingerprintCancellationSignal, fingerprintListener, null);
|
||||
} else if (Build.VERSION.SDK_INT >= 21){
|
||||
Log.i(TAG, "firing intent...");
|
||||
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
|
||||
startActivityForResult(intent, 1);
|
||||
} else {
|
||||
Log.w(TAG, "Not compatible...");
|
||||
handleAuthenticated();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEmailToSupport() {
|
||||
String body = SupportEmailUtil.generateSupportEmailBody(this,
|
||||
R.string.PassphrasePromptActivity_signal_android_lock_screen,
|
||||
null,
|
||||
null);
|
||||
CommunicationActions.openEmail(this,
|
||||
SupportEmailUtil.getSupportEmailAddress(this),
|
||||
getString(R.string.PassphrasePromptActivity_signal_android_lock_screen),
|
||||
body);
|
||||
private void pauseScreenLock() {
|
||||
if (fingerprintCancellationSignal != null) {
|
||||
fingerprintCancellationSignal.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private class PassphraseActionListener implements TextView.OnEditorActionListener {
|
||||
@@ -380,19 +342,15 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
System.gc();
|
||||
}
|
||||
|
||||
private class BiometricAuthenticationListener extends BiometricPrompt.AuthenticationCallback {
|
||||
private class FingerprintListener extends FingerprintManagerCompat.AuthenticationCallback {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errorString) {
|
||||
Log.w(TAG, "Authentication error: " + errorCode);
|
||||
hadFailure = true;
|
||||
|
||||
if (errorCode != BiometricPrompt.ERROR_CANCELED && errorCode != BiometricPrompt.ERROR_USER_CANCELED) {
|
||||
onAuthenticationFailed();
|
||||
}
|
||||
public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
||||
Log.w(TAG, "Authentication error: " + errMsgId + " " + errString);
|
||||
onAuthenticationFailed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
|
||||
Log.i(TAG, "onAuthenticationSucceeded");
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
|
||||
@@ -400,13 +358,17 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
handleAuthenticated();
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
Log.w(TAG, "onAuthenticationFailed()");
|
||||
Log.w(TAG, "onAuthenticatoinFailed()");
|
||||
FingerprintManagerCompat.AuthenticationCallback callback = this;
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_close_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
|
||||
@@ -421,7 +383,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -430,5 +392,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
|
||||
fingerprintPrompt.startAnimation(shake);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,9 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
@@ -30,13 +25,13 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.tracing.Tracer;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class PassphraseRequiredActivity extends BaseActivity implements MasterSecretListener {
|
||||
private static final String TAG = Log.tag(PassphraseRequiredActivity.class);
|
||||
private static final String TAG = PassphraseRequiredActivity.class.getSimpleName();
|
||||
|
||||
public static final String LOCALE_EXTRA = "locale_extra";
|
||||
public static final String NEXT_INTENT_EXTRA = "next_intent";
|
||||
@@ -49,9 +44,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
private static final int STATE_ENTER_SIGNAL_PIN = 5;
|
||||
private static final int STATE_CREATE_PROFILE_NAME = 6;
|
||||
private static final int STATE_CREATE_SIGNAL_PIN = 7;
|
||||
private static final int STATE_TRANSFER_ONGOING = 8;
|
||||
private static final int STATE_TRANSFER_LOCKED = 9;
|
||||
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -59,8 +51,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
@Override
|
||||
protected final void onCreate(Bundle savedInstanceState) {
|
||||
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
|
||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
|
||||
this.networkAccess = new SignalServiceNetworkAccess(this);
|
||||
onPreCreate();
|
||||
|
||||
final boolean locked = KeyCachingService.isLocked(this);
|
||||
@@ -72,8 +63,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
initializeClearKeyReceiver();
|
||||
onCreate(savedInstanceState, true);
|
||||
}
|
||||
|
||||
AppStartup.getInstance().onCriticalRenderEventEnd();
|
||||
Tracer.getInstance().end(Log.tag(getClass()) + "#onCreate()");
|
||||
}
|
||||
|
||||
@@ -84,8 +73,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (networkAccess.isCensored()) {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
|
||||
if (networkAccess.isCensored(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +87,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
@Override
|
||||
public void onMasterSecretCleared() {
|
||||
Log.d(TAG, "onMasterSecretCleared()");
|
||||
if (ApplicationDependencies.getAppForegroundObserver().isForegrounded()) routeApplicationState(true);
|
||||
else finish();
|
||||
if (ApplicationContext.getInstance(this).isAppVisible()) routeApplicationState(true);
|
||||
else finish();
|
||||
}
|
||||
|
||||
protected <T extends Fragment> T initFragment(@IdRes int target,
|
||||
@@ -153,9 +142,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
case STATE_ENTER_SIGNAL_PIN: return getEnterSignalPinIntent();
|
||||
case STATE_CREATE_SIGNAL_PIN: return getCreateSignalPinIntent();
|
||||
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
|
||||
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
|
||||
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -169,20 +155,12 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_UI_BLOCKING_UPGRADE;
|
||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||
return STATE_WELCOME_PUSH_SCREEN;
|
||||
} else if (SignalStore.storageService().needsAccountRestore()) {
|
||||
} else if (SignalStore.storageServiceValues().needsAccountRestore()) {
|
||||
return STATE_ENTER_SIGNAL_PIN;
|
||||
} else if (userHasSkippedOrForgottenPin()) {
|
||||
return STATE_CREATE_SIGNAL_PIN;
|
||||
} else if (userMustSetProfileName()) {
|
||||
return STATE_CREATE_PROFILE_NAME;
|
||||
} else if (userMustCreateSignalPin()) {
|
||||
return STATE_CREATE_SIGNAL_PIN;
|
||||
} else if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null && getClass() != OldDeviceTransferActivity.class) {
|
||||
return STATE_TRANSFER_ONGOING;
|
||||
} else if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||
return STATE_TRANSFER_LOCKED;
|
||||
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
|
||||
return STATE_CHANGE_NUMBER_LOCK;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
@@ -192,10 +170,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed() && !SignalStore.kbsValues().hasOptedOut();
|
||||
}
|
||||
|
||||
private boolean userHasSkippedOrForgottenPin() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().hasOptedOut() && SignalStore.kbsValues().isPinForgottenOrSkipped();
|
||||
}
|
||||
|
||||
private boolean userMustSetProfileName() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
|
||||
}
|
||||
@@ -205,9 +179,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private Intent getPromptPassphraseIntent() {
|
||||
Intent intent = getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
intent.putExtra(PassphrasePromptActivity.FROM_FOREGROUND, ApplicationDependencies.getAppForegroundObserver().isForegrounded());
|
||||
return intent;
|
||||
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getUiBlockingUpgradeIntent() {
|
||||
@@ -218,7 +190,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
||||
}
|
||||
|
||||
private Intent getEnterSignalPinIntent() {
|
||||
@@ -241,23 +213,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return getRoutedIntent(EditProfileActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getOldDeviceTransferIntent() {
|
||||
Intent intent = new Intent(this, OldDeviceTransferActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private @Nullable Intent getOldDeviceTransferLockedIntent() {
|
||||
if (getClass() == MainActivity.class) {
|
||||
return null;
|
||||
}
|
||||
return MainActivity.clearTop(this);
|
||||
}
|
||||
|
||||
private Intent getChangeNumberLockIntent() {
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
@@ -266,17 +221,15 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
|
||||
private Intent getConversationListIntent() {
|
||||
// TODO [greyson] Navigation
|
||||
return MainActivity.clearTop(this);
|
||||
return new Intent(this, MainActivity.class);
|
||||
}
|
||||
|
||||
private void initializeClearKeyReceiver() {
|
||||
this.clearKeyReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "onReceive() for clear key event. PasswordDisabled: " + TextSecurePreferences.isPasswordDisabled(context) + ", ScreenLock: " + TextSecurePreferences.isScreenLockEnabled(context));
|
||||
if (TextSecurePreferences.isScreenLockEnabled(context) || !TextSecurePreferences.isPasswordDisabled(context)) {
|
||||
onMasterSecretCleared();
|
||||
}
|
||||
Log.i(TAG, "onReceive() for clear key event");
|
||||
onMasterSecretCleared();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.os.Bundle;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
@@ -39,7 +38,7 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
||||
public static final String KEY_SELECTED_RECIPIENTS = "recipients";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = Log.tag(PushContactSelectionActivity.class);
|
||||
private final static String TAG = PushContactSelectionActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
@@ -65,8 +64,4 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
|
||||
if (rawId == null) {
|
||||
Toast.makeText(this, R.string.ShortcutLauncherActivity_invalid_shortcut, Toast.LENGTH_SHORT).show();
|
||||
// TODO [greyson] Navigation
|
||||
startActivity(MainActivity.clearTop(this));
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
|
||||
Recipient recipient = Recipient.live(RecipientId.from(rawId)).get();
|
||||
// TODO [greyson] Navigation
|
||||
TaskStackBuilder backStack = TaskStackBuilder.create(this)
|
||||
.addNextIntent(MainActivity.clearTop(this));
|
||||
.addNextIntent(new Intent(this, MainActivity.class));
|
||||
|
||||
CommunicationActions.startConversation(this, recipient, null, backStack);
|
||||
finish();
|
||||
|
||||