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/
|
||||
@@ -1,12 +1,3 @@
|
||||
---
|
||||
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.
|
||||
|
||||
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.
|
||||
19
.github/workflows/android.yml
vendored
@@ -14,25 +14,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: set up JDK 11
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
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@v2
|
||||
with:
|
||||
name: reports
|
||||
path: '*/build/reports'
|
||||
|
||||
225
.idea/codeStyles/Project.xml
generated
@@ -1,225 +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 />
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<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>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</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>
|
||||
@@ -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>
|
||||
|
||||
@@ -59,8 +59,8 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2021 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.
|
||||
|
||||
485
app/build.gradle
@@ -1,16 +1,27 @@
|
||||
import org.signal.signing.ApkSignerUtil
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'androidx.navigation.safeargs'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
apply from: 'translations.gradle'
|
||||
apply from: 'witness-verifications.gradle'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'app.cash.exhaustive'
|
||||
|
||||
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 {
|
||||
@@ -29,26 +40,10 @@ repositories {
|
||||
includeGroupByRegex "com\\.amulyakhare.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
maven {
|
||||
url "https://dl.cloudsmith.io/qxAgwaeEE1vN8aLU/mobilecoin/mobilecoin/maven/"
|
||||
}
|
||||
jcenter {
|
||||
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.amulyakhare", "com.amulyakhare.textdrawable", "1.0.1"
|
||||
includeVersion "com.google.android", "flexbox", "0.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
@@ -66,43 +61,18 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 929
|
||||
def canonicalVersionName = "5.24.9"
|
||||
def canonicalVersionCode = 759
|
||||
def canonicalVersionName = "5.0.10"
|
||||
|
||||
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 = [
|
||||
'internalProdFlipper',
|
||||
'internalProdPerf',
|
||||
'internalProdRelease',
|
||||
'internalStagingFlipper',
|
||||
'internalStagingPerf',
|
||||
'internalStagingRelease',
|
||||
'nightlyProdFlipper',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'nightlyStagingPerf',
|
||||
'playProdDebug',
|
||||
'playProdFlipper',
|
||||
'playProdPerf',
|
||||
'playProdRelease',
|
||||
'playStagingDebug',
|
||||
'playStagingFlipper',
|
||||
'playStagingPerf',
|
||||
'playStagingRelease',
|
||||
'studyProdMock',
|
||||
'studyProdPerf',
|
||||
'websiteProdFlipper',
|
||||
'websiteProdRelease',
|
||||
]
|
||||
|
||||
android {
|
||||
buildToolsVersion BUILD_TOOL_VERSION
|
||||
compileSdkVersion COMPILE_SDK
|
||||
@@ -110,11 +80,6 @@ android {
|
||||
flavorDimensions 'distribution', 'environment'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = ["-Xallow-result-return-type"]
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
javaMaxHeapSize "4g"
|
||||
}
|
||||
@@ -143,23 +108,17 @@ android {
|
||||
project.ext.set("archivesBaseName", "Signal");
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
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_CDSH_URL", "\"https://cdsh.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_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
@@ -169,14 +128,7 @@ android {
|
||||
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 "int[]", "MOBILE_COIN_REGIONS", "new int[]{44,49,33,41}"
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
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 "int", "TRACE_EVENT_MAX", "3500"
|
||||
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
@@ -197,7 +149,6 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JAVA_VERSION
|
||||
targetCompatibility JAVA_VERSION
|
||||
}
|
||||
@@ -210,8 +161,10 @@ android {
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
exclude '/org/spongycastle/x509/CertPathReviewerMessages.properties'
|
||||
exclude '/org/spongycastle/x509/CertPathReviewerMessages_de.properties'
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -242,34 +195,16 @@ android {
|
||||
'proguard/proguard.cfg'
|
||||
testProguardFiles 'proguard/proguard-automation.pro',
|
||||
'proguard/proguard.cfg'
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||
}
|
||||
flipper {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles = buildTypes.debug.proguardFiles
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
|
||||
}
|
||||
perf {
|
||||
initWith debug
|
||||
isDefault false
|
||||
debuggable false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
|
||||
}
|
||||
mock {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Mock\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,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 {
|
||||
@@ -288,7 +222,6 @@ 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\""
|
||||
}
|
||||
|
||||
internal {
|
||||
@@ -296,35 +229,13 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
|
||||
}
|
||||
|
||||
nightly {
|
||||
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\""
|
||||
}
|
||||
|
||||
study {
|
||||
dimension 'distribution'
|
||||
|
||||
applicationIdSuffix ".study"
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"study\""
|
||||
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 {
|
||||
@@ -340,46 +251,23 @@ android {
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
|
||||
"\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if (!selectableVariants.contains(fullName)) {
|
||||
variant.setIgnore(true)
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,170 +285,198 @@ android {
|
||||
}
|
||||
|
||||
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(':video')
|
||||
implementation project(':device-transfer')
|
||||
implementation project(':image-editor')
|
||||
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.signal.zkgroup.android
|
||||
implementation libs.signal.client.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
|
||||
kapt libs.glide.compiler
|
||||
kapt libs.androidx.annotation
|
||||
implementation libs.roundedimageview
|
||||
implementation libs.materialish.progress
|
||||
implementation libs.greenrobot.eventbus
|
||||
implementation libs.waitingdots
|
||||
implementation libs.floatingactionbutton
|
||||
implementation libs.google.zxing.android.integration
|
||||
implementation libs.time.duration.picker
|
||||
implementation libs.textdrawable
|
||||
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.circular.progress.button
|
||||
|
||||
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'
|
||||
|
||||
flipperImplementation libs.facebook.flipper
|
||||
flipperImplementation libs.facebook.soloader
|
||||
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.powermock.api.mockito
|
||||
testImplementation testLibs.powermock.module.junit4.core
|
||||
testImplementation testLibs.powermock.module.junit4.rule
|
||||
testImplementation testLibs.powermock.classloading.xstream
|
||||
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.hamcrest.hamcrest
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
|
||||
androidTestImplementation testLibs.androidx.test.ext.junit
|
||||
androidTestImplementation testLibs.espresso.core
|
||||
|
||||
implementation libs.kotlin.stdlib.jdk8
|
||||
implementation libs.kotlin.reflect
|
||||
implementation libs.jackson.module.kotlin
|
||||
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
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 getLastCommitTimestamp() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return System.currentTimeMillis().toString()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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', ''))
|
||||
|
||||
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
|
||||
'pkcs11.config',
|
||||
'PKCS11',
|
||||
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
|
||||
outputFile.getAbsolutePath())
|
||||
|
||||
inputFile.delete()
|
||||
outputFile
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionPlayRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionInternalRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionWebsiteRelease {
|
||||
doLast {
|
||||
def variant = android.applicationVariants.find { (it.name == 'websiteProdRelease') }
|
||||
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
||||
assembleWebsiteDescriptor(variant, signedRelease)
|
||||
}
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
def result = exec {
|
||||
executable = 'git'
|
||||
@@ -572,39 +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) {
|
||||
return output.split('\n')[0];
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
testLogging {
|
||||
events "failed"
|
||||
@@ -625,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.**
|
||||
@@ -9,5 +9,3 @@
|
||||
|
||||
# Protobuf lite
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
-keep class androidx.window.** { *; }
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
@@ -11,13 +10,12 @@ import androidx.annotation.Nullable;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
||||
|
||||
import net.zetetic.database.DatabaseUtils;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
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 org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
@@ -26,7 +24,6 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
|
||||
@@ -45,18 +42,8 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
try {
|
||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||
databaseHelperField.setAccessible(true);
|
||||
|
||||
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())));
|
||||
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
||||
|
||||
return Arrays.asList(new Descriptor(mainOpenHelper),
|
||||
new Descriptor(keyValueOpenHelper),
|
||||
new Descriptor(megaphoneOpenHelper),
|
||||
new Descriptor(jobManagerOpenHelper),
|
||||
new Descriptor(metricsOpenHelper));
|
||||
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);
|
||||
}
|
||||
@@ -240,12 +227,7 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
case Cursor.FIELD_TYPE_FLOAT:
|
||||
return cursor.getDouble(column);
|
||||
case Cursor.FIELD_TYPE_BLOB:
|
||||
byte[] blob = cursor.getBlob(column);
|
||||
String bytes = blob != null ? "(blob) " + Hex.toStringCondensed(Arrays.copyOf(blob, Math.min(blob.length, 32))) : null;
|
||||
if (bytes != null && bytes.length() == 32 && blob.length > 32) {
|
||||
bytes += "...";
|
||||
}
|
||||
return bytes;
|
||||
return cursor.getBlob(column);
|
||||
case Cursor.FIELD_TYPE_STRING:
|
||||
default:
|
||||
return cursor.getString(column);
|
||||
@@ -253,9 +235,9 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
}
|
||||
|
||||
static class Descriptor implements DatabaseDescriptor {
|
||||
private final SignalDatabase sqlCipherOpenHelper;
|
||||
private final SQLCipherOpenHelper sqlCipherOpenHelper;
|
||||
|
||||
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) {
|
||||
Descriptor(@NonNull SQLCipherOpenHelper sqlCipherOpenHelper) {
|
||||
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
|
||||
}
|
||||
|
||||
@@ -265,11 +247,11 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
}
|
||||
|
||||
public @NonNull SQLiteDatabase getReadable() {
|
||||
return sqlCipherOpenHelper.getSqlCipherDatabase();
|
||||
return sqlCipherOpenHelper.getReadableDatabase().getSqlCipherDatabase();
|
||||
}
|
||||
|
||||
public @NonNull SQLiteDatabase getWritable() {
|
||||
return sqlCipherOpenHelper.getSqlCipherDatabase();
|
||||
return sqlCipherOpenHelper.getWritableDatabase().getSqlCipherDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
@@ -96,7 +97,6 @@
|
||||
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">
|
||||
@@ -105,9 +105,6 @@
|
||||
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" />
|
||||
|
||||
@@ -121,15 +118,16 @@
|
||||
<activity android:name=".WebRtcCallActivity"
|
||||
android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
|
||||
android:excludeFromRecents="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:taskAffinity=".calling"
|
||||
android:resizeableActivity="true"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:screenOrientation="portrait"
|
||||
android:noHistory="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -156,27 +154,17 @@
|
||||
<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.ShareActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
@@ -200,7 +188,7 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
android:value=".service.DirectShareService" />
|
||||
|
||||
</activity>
|
||||
|
||||
@@ -237,20 +225,6 @@
|
||||
<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>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -268,26 +242,12 @@
|
||||
android:host="signal.group"/>
|
||||
</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="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>
|
||||
<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"
|
||||
@@ -317,17 +277,19 @@
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar" />
|
||||
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"
|
||||
@@ -335,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"/>
|
||||
|
||||
@@ -366,61 +328,27 @@
|
||||
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=".VerifyIdentityActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
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=".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|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.ConversationSettings"
|
||||
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"
|
||||
@@ -499,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">
|
||||
@@ -521,21 +448,13 @@
|
||||
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"
|
||||
@@ -585,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"
|
||||
@@ -611,24 +531,7 @@
|
||||
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"/>
|
||||
<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"/>
|
||||
@@ -673,6 +576,13 @@
|
||||
<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.FcmFetchService" />
|
||||
@@ -732,16 +642,26 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<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=".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"
|
||||
@@ -770,6 +690,10 @@
|
||||
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" />
|
||||
@@ -807,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: 86 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 664 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 631 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 653 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 652 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 685 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 195 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,17 +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 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, FeatureFlags.changeNumber());
|
||||
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;
|
||||
@@ -44,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()));
|
||||
}
|
||||
@@ -54,34 +52,8 @@ public final class AppInitialization {
|
||||
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
SignalStore.onboarding().clearAll();
|
||||
TextSecurePreferences.onPostBackupRestore(context);
|
||||
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.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
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,38 +16,35 @@
|
||||
*/
|
||||
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.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||
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.gcm.FcmJobService;
|
||||
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.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
@@ -56,42 +53,39 @@ import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
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.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
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.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.security.Security;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Will be called once when the TextSecure process is created.
|
||||
*
|
||||
@@ -100,11 +94,16 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
*
|
||||
* @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();
|
||||
@@ -113,108 +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())
|
||||
.addBlocking("logging", () -> {
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
})
|
||||
.addBlocking("crash-handling", this::initializeCrashHandling)
|
||||
.addBlocking("rx-init", () -> {
|
||||
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
|
||||
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
|
||||
})
|
||||
.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)
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
.addNonBlocking(this::initializeGcmCheck)
|
||||
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
||||
.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))
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.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.");
|
||||
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(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().end();
|
||||
ApplicationDependencies.getShakeToReport().disable();
|
||||
}
|
||||
|
||||
public ExpiringMessageManager getExpiringMessageManager() {
|
||||
return expiringMessageManager;
|
||||
}
|
||||
|
||||
public ViewOnceMessageManager getViewOnceMessageManager() {
|
||||
return viewOnceMessageManager;
|
||||
}
|
||||
|
||||
public boolean isAppVisible() {
|
||||
return isAppVisible;
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
@@ -253,12 +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(() -> LogDatabase.getInstance(this).trimToSize());
|
||||
}
|
||||
|
||||
private void initializeCrashHandling() {
|
||||
@@ -275,24 +247,18 @@ 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 (!SQLCipherOpenHelper.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,15 +279,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -329,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);
|
||||
@@ -338,11 +299,31 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
private void initializeRingRtc() {
|
||||
try {
|
||||
if (RtcDeviceLists.hardwareAECBlocked()) {
|
||||
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 (!RtcDeviceLists.openSLESAllowed()) {
|
||||
if (!OPEN_SL_ES_WHITELIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
|
||||
}
|
||||
|
||||
@@ -352,15 +333,23 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void initializeCircumvention() {
|
||||
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void executePendingContactSync() {
|
||||
@@ -375,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 = DatabaseFactory.getAttachmentDatabase(this).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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,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;
|
||||
@@ -72,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));
|
||||
@@ -125,7 +122,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
}
|
||||
});
|
||||
|
||||
title.setText(recipient.getDisplayName(context));
|
||||
toolbar.setTitle(recipient.getDisplayName(context));
|
||||
});
|
||||
|
||||
FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
|
||||
|
||||
@@ -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;
|
||||
@@ -32,30 +26,22 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
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 pulseMention);
|
||||
|
||||
@NonNull ConversationMessage getConversationMessage();
|
||||
ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
default void updateTimestamps() {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
@@ -69,25 +55,13 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
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);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
@@ -9,8 +9,6 @@ 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.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
@@ -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,11 +55,11 @@ 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()) {
|
||||
@@ -80,10 +78,10 @@ public final class BlockUnblockDialog {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||
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);
|
||||
@@ -100,7 +98,7 @@ 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()) {
|
||||
|
||||
@@ -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,23 +20,21 @@ import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
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.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.function.Consumer;
|
||||
|
||||
/**
|
||||
* Base activity container for selecting a list of contacts.
|
||||
@@ -49,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";
|
||||
|
||||
@@ -57,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() {
|
||||
@@ -68,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();
|
||||
@@ -87,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
|
||||
@@ -123,8 +113,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
callback.accept(true);
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -165,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -38,7 +37,6 @@ 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;
|
||||
@@ -55,19 +53,16 @@ import androidx.transition.TransitionManager;
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper;
|
||||
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
|
||||
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.LetterHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
@@ -78,9 +73,9 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.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.concurrent.SimpleTask;
|
||||
@@ -91,7 +86,6 @@ import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
@@ -116,30 +110,22 @@ 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 ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
||||
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
||||
private View shadowView;
|
||||
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
|
||||
|
||||
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;
|
||||
@@ -150,7 +136,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
private boolean canSelectSelf;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -160,37 +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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -219,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();
|
||||
@@ -242,12 +199,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
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);
|
||||
shadowView = view.findViewById(R.id.toolbar_shadow);
|
||||
|
||||
toolbarShadowAnimationHelper = new ToolbarShadowAnimationHelper(shadowView);
|
||||
|
||||
recyclerView.addOnScrollListener(toolbarShadowAnimationHelper);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
recyclerView.setItemAnimator(new DefaultItemAnimator() {
|
||||
@Override
|
||||
@@ -256,29 +210,13 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
});
|
||||
|
||||
Intent intent = requireActivity().getIntent();
|
||||
Bundle arguments = safeArguments();
|
||||
Intent intent = requireActivity().getIntent();
|
||||
|
||||
int recyclerViewPadBottom = arguments.getInt(RV_PADDING_BOTTOM, intent.getIntExtra(RV_PADDING_BOTTOM, -1));
|
||||
boolean recyclerViewClipping = arguments.getBoolean(RV_CLIP, intent.getBooleanExtra(RV_CLIP, true));
|
||||
swipeRefresh.setEnabled(intent.getBooleanExtra(REFRESHABLE, 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;
|
||||
@@ -286,11 +224,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
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
|
||||
@@ -314,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()));
|
||||
@@ -362,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) {
|
||||
@@ -374,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) {
|
||||
@@ -446,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
|
||||
@@ -500,10 +415,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private boolean shouldDisplayRecents() {
|
||||
return safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleContactPermissionGranted() {
|
||||
final Context context = requireContext();
|
||||
@@ -537,11 +448,8 @@ 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();
|
||||
@@ -553,18 +461,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
|
||||
if (!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;
|
||||
}
|
||||
|
||||
@@ -580,40 +484,36 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
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());
|
||||
@@ -639,19 +539,12 @@ 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);
|
||||
removeChipForContact(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeChipForContact(@NonNull SelectedContact contact) {
|
||||
@@ -662,6 +555,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
}
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.GONE);
|
||||
}
|
||||
@@ -698,11 +593,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
@Override
|
||||
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||
if (getView() == null || !requireView().isAttachedToWindow()) {
|
||||
Log.w(TAG, "Fragment's view was detached before the animation completed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (view == chip && transitionType == LayoutTransition.APPEARING) {
|
||||
chipGroup.getLayoutTransition().removeTransitionListener(this);
|
||||
registerChipRecipientObserver(chip, recipient.live());
|
||||
@@ -716,12 +606,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
private void addChip(@NonNull ContactChip chip) {
|
||||
chipGroup.addView(chip);
|
||||
updateGroupLimit(getChipCount());
|
||||
if (selectionWarningLimitReachedExactly()) {
|
||||
if (onSelectionLimitReachedListener != null) {
|
||||
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
|
||||
} else {
|
||||
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||
}
|
||||
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,10 +630,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
private void setChipGroupVisibility(int visibility) {
|
||||
if (!safeArguments().getBoolean(DISPLAY_CHIPS, requireActivity().getIntent().getBooleanExtra(DISPLAY_CHIPS, true))) {
|
||||
return;
|
||||
}
|
||||
|
||||
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
@@ -755,25 +638,23 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
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(chipGroupScrollContainer) ? chipGroup.getWidth() : 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(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback);
|
||||
/** @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);
|
||||
void onSelectionChanged();
|
||||
}
|
||||
|
||||
public interface OnSelectionLimitReachedListener {
|
||||
void onSuggestedLimitReached(int limit);
|
||||
void onHardLimitReached(int limit);
|
||||
}
|
||||
|
||||
public interface ListCallback {
|
||||
@@ -784,8 +665,4 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public interface ScrollCallback {
|
||||
void onBeginScroll();
|
||||
}
|
||||
|
||||
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,10 +16,8 @@ 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.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
@@ -28,9 +26,9 @@ 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;
|
||||
@@ -47,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;
|
||||
@@ -64,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();
|
||||
@@ -80,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
|
||||
@@ -95,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;
|
||||
@@ -111,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();
|
||||
})
|
||||
@@ -121,12 +123,12 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onQrDataFound(final String data) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
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));
|
||||
|
||||
@@ -136,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);
|
||||
|
||||
@@ -32,9 +32,9 @@ public class DeviceAddFragment extends LoggingFragment {
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
|
||||
this.overlay = this.container.findViewById(R.id.overlay);
|
||||
this.scannerView = this.container.findViewById(R.id.scanner);
|
||||
this.devicesImage = this.container.findViewById(R.id.devices);
|
||||
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);
|
||||
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
this.overlay.setOrientation(LinearLayout.HORIZONTAL);
|
||||
@@ -42,9 +42,9 @@ public class DeviceAddFragment extends LoggingFragment {
|
||||
this.overlay.setOrientation(LinearLayout.VERTICAL);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
@TargetApi(21)
|
||||
@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)
|
||||
@@ -80,7 +80,7 @@ public class DeviceAddFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
|
||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
|
||||
this.scannerView.onPause();
|
||||
@@ -107,4 +107,6 @@ public class DeviceAddFragment extends LoggingFragment {
|
||||
this.scanningThread.setScanListener(scanListener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -32,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);
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.fragment.app.ListFragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -29,6 +28,7 @@ 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;
|
||||
|
||||
@@ -41,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;
|
||||
@@ -53,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();
|
||||
}
|
||||
|
||||
@@ -68,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;
|
||||
@@ -122,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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,16 +14,16 @@ 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.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.DatabaseFactory;
|
||||
@@ -32,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.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() {
|
||||
@@ -78,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;
|
||||
@@ -91,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) {
|
||||
@@ -134,9 +122,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||
callback.accept(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,10 +132,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
|
||||
private void sendSmsInvites() {
|
||||
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
@@ -156,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);
|
||||
}
|
||||
|
||||
@@ -168,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) {
|
||||
@@ -201,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);
|
||||
}
|
||||
}
|
||||
@@ -252,7 +261,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
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) {
|
||||
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,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -9,64 +8,36 @@ import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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.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;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
navigator.onCreate(savedInstanceState);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
handleProxyInIntent(getIntent());
|
||||
handleSignalMeIntent(getIntent());
|
||||
|
||||
CachedInflater.from(this).clear();
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -79,9 +50,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||
OldDeviceTransferLockedDialog.show(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity;
|
||||
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;
|
||||
@@ -71,7 +69,8 @@ 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() {
|
||||
|
||||
@@ -91,7 +91,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
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;
|
||||
|
||||
@@ -103,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;
|
||||
@@ -116,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;
|
||||
@@ -141,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;
|
||||
}
|
||||
@@ -171,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);
|
||||
@@ -300,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() {
|
||||
@@ -359,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);
|
||||
@@ -637,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
|
||||
@@ -665,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ 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 {
|
||||
@@ -31,7 +29,7 @@ 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, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
@@ -40,10 +38,10 @@ public class MuteDialog extends AlertDialog {
|
||||
|
||||
switch (which) {
|
||||
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); 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 = Long.MAX_VALUE; break;
|
||||
case 4: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365); break;
|
||||
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Activity container for starting a new conversation.
|
||||
@@ -50,18 +49,17 @@ 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(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
@@ -95,11 +93,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
}
|
||||
|
||||
callback.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,7 +51,6 @@ 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 = new SignalServiceNetworkAccess(this);
|
||||
onPreCreate();
|
||||
|
||||
@@ -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()");
|
||||
}
|
||||
|
||||
@@ -85,7 +74,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
super.onResume();
|
||||
|
||||
if (networkAccess.isCensored(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
|
||||
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,18 +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 (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;
|
||||
}
|
||||
@@ -199,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() {
|
||||
@@ -212,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() {
|
||||
@@ -235,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);
|
||||
@@ -260,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() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Device hardware capability lists.
|
||||
* <p>
|
||||
* Moved outside of ApplicationContext as the indirection was important for API19 support with desugaring: https://issuetracker.google.com/issues/183419297
|
||||
*/
|
||||
final class RtcDeviceLists {
|
||||
|
||||
private RtcDeviceLists() {}
|
||||
|
||||
static Set<String> hardwareAECBlockList() {
|
||||
return 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");
|
||||
}};
|
||||
}
|
||||
|
||||
static Set<String> openSlEsAllowList() {
|
||||
return new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
}};
|
||||
}
|
||||
|
||||
static boolean hardwareAECBlocked() {
|
||||
return hardwareAECBlockList().contains(Build.MODEL);
|
||||
}
|
||||
|
||||
static boolean openSLESAllowed() {
|
||||
return openSlEsAllowList().contains(Build.MODEL);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.net.URISyntaxException;
|
||||
|
||||
public class SmsSendtoActivity extends Activity {
|
||||
|
||||
private static final String TAG = Log.tag(SmsSendtoActivity.class);
|
||||
private static final String TAG = SmsSendtoActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.MmsCharacterCalculator;
|
||||
@@ -26,7 +25,7 @@ import static org.thoughtcrime.securesms.TransportOption.Type;
|
||||
|
||||
public class TransportOptions {
|
||||
|
||||
private static final String TAG = Log.tag(TransportOptions.class);
|
||||
private static final String TAG = TransportOptions.class.getSimpleName();
|
||||
|
||||
private final List<OnTransportChangedListener> listeners = new LinkedList<>();
|
||||
private final Context context;
|
||||
|
||||
@@ -11,6 +11,8 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TransportOptionsAdapter extends BaseAdapter {
|
||||
@@ -53,9 +55,9 @@ public class TransportOptionsAdapter extends BaseAdapter {
|
||||
}
|
||||
|
||||
TransportOption transport = (TransportOption) getItem(position);
|
||||
ImageView imageView = convertView.findViewById(R.id.icon);
|
||||
TextView textView = convertView.findViewById(R.id.text);
|
||||
TextView subtextView = convertView.findViewById(R.id.subtext);
|
||||
ImageView imageView = ViewUtil.findById(convertView, R.id.icon);
|
||||
TextView textView = ViewUtil.findById(convertView, R.id.text);
|
||||
TextView subtextView = ViewUtil.findById(convertView, R.id.subtext);
|
||||
|
||||
imageView.getBackground().setColorFilter(transport.getBackgroundColor(), Mode.MULTIPLY);
|
||||
imageView.setImageResource(transport.getDrawable());
|
||||
|
||||
@@ -29,6 +29,7 @@ import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
@@ -43,38 +44,30 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnticipateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextSwitcher;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.OneShotPreDrawListener;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ShapeScrim;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -85,23 +78,27 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
|
||||
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
/**
|
||||
* Activity for verifying identity keys.
|
||||
*
|
||||
@@ -116,13 +113,13 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
private static final String IDENTITY_EXTRA = "recipient_identity";
|
||||
private static final String VERIFIED_EXTRA = "verified_state";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
|
||||
|
||||
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
|
||||
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityRecord identityRecord)
|
||||
@NonNull IdentityDatabase.IdentityRecord identityRecord)
|
||||
{
|
||||
return newIntent(context,
|
||||
identityRecord.getRecipientId(),
|
||||
@@ -131,7 +128,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityRecord identityRecord,
|
||||
@NonNull IdentityDatabase.IdentityRecord identityRecord,
|
||||
boolean verified)
|
||||
{
|
||||
return newIntent(context,
|
||||
@@ -161,6 +158,14 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
|
||||
|
||||
LiveRecipient recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
recipient.observe(this, r -> setActionBarNotificationBarColor(r.getColor()));
|
||||
|
||||
setActionBarNotificationBarColor(recipient.get().getColor());
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
|
||||
@@ -185,7 +190,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
@Override
|
||||
public void onQrDataFound(final String data) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
Util.runOnMain(() -> {
|
||||
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
|
||||
|
||||
getSupportFragmentManager().popBackStack();
|
||||
@@ -212,13 +217,18 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
.execute();
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener {
|
||||
private void setActionBarNotificationBarColor(MaterialColor color) {
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
|
||||
WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
public static final String REMOTE_NUMBER = "remote_number";
|
||||
@@ -232,73 +242,50 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
private IdentityKey remoteIdentity;
|
||||
private Fingerprint fingerprint;
|
||||
|
||||
private Toolbar toolbar;
|
||||
private ScrollView scrollView;
|
||||
private View container;
|
||||
private View numbersContainer;
|
||||
private View loading;
|
||||
private View qrCodeContainer;
|
||||
private ImageView qrCode;
|
||||
private ImageView qrVerified;
|
||||
private TextSwitcher tapLabel;
|
||||
private TextView tapLabel;
|
||||
private TextView description;
|
||||
private View.OnClickListener clickListener;
|
||||
private Button verifyButton;
|
||||
private View toolbarShadow;
|
||||
private View bottomShadow;
|
||||
private SwitchCompat verified;
|
||||
|
||||
private TextView[] codes = new TextView[12];
|
||||
private boolean animateSuccessOnDraw = false;
|
||||
private boolean animateFailureOnDraw = false;
|
||||
private boolean currentVerifiedState = false;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
|
||||
this.toolbar = container.findViewById(R.id.toolbar);
|
||||
this.scrollView = container.findViewById(R.id.scroll_view);
|
||||
this.numbersContainer = container.findViewById(R.id.number_table);
|
||||
this.loading = container.findViewById(R.id.loading);
|
||||
this.qrCodeContainer = container.findViewById(R.id.qr_code_container);
|
||||
this.qrCode = container.findViewById(R.id.qr_code);
|
||||
this.verifyButton = container.findViewById(R.id.verify_button);
|
||||
this.qrVerified = container.findViewById(R.id.qr_verified);
|
||||
this.description = container.findViewById(R.id.description);
|
||||
this.tapLabel = container.findViewById(R.id.tap_label);
|
||||
this.toolbarShadow = container.findViewById(R.id.toolbar_shadow);
|
||||
this.bottomShadow = container.findViewById(R.id.verify_identity_bottom_shadow);
|
||||
this.codes[0] = container.findViewById(R.id.code_first);
|
||||
this.codes[1] = container.findViewById(R.id.code_second);
|
||||
this.codes[2] = container.findViewById(R.id.code_third);
|
||||
this.codes[3] = container.findViewById(R.id.code_fourth);
|
||||
this.codes[4] = container.findViewById(R.id.code_fifth);
|
||||
this.codes[5] = container.findViewById(R.id.code_sixth);
|
||||
this.codes[6] = container.findViewById(R.id.code_seventh);
|
||||
this.codes[7] = container.findViewById(R.id.code_eighth);
|
||||
this.codes[8] = container.findViewById(R.id.code_ninth);
|
||||
this.codes[9] = container.findViewById(R.id.code_tenth);
|
||||
this.codes[10] = container.findViewById(R.id.code_eleventh);
|
||||
this.codes[11] = container.findViewById(R.id.code_twelth);
|
||||
this.numbersContainer = ViewUtil.findById(container, R.id.number_table);
|
||||
this.qrCode = ViewUtil.findById(container, R.id.qr_code);
|
||||
this.verified = ViewUtil.findById(container, R.id.verified_switch);
|
||||
this.qrVerified = ViewUtil.findById(container, R.id.qr_verified);
|
||||
this.description = ViewUtil.findById(container, R.id.description);
|
||||
this.tapLabel = ViewUtil.findById(container, R.id.tap_label);
|
||||
this.codes[0] = ViewUtil.findById(container, R.id.code_first);
|
||||
this.codes[1] = ViewUtil.findById(container, R.id.code_second);
|
||||
this.codes[2] = ViewUtil.findById(container, R.id.code_third);
|
||||
this.codes[3] = ViewUtil.findById(container, R.id.code_fourth);
|
||||
this.codes[4] = ViewUtil.findById(container, R.id.code_fifth);
|
||||
this.codes[5] = ViewUtil.findById(container, R.id.code_sixth);
|
||||
this.codes[6] = ViewUtil.findById(container, R.id.code_seventh);
|
||||
this.codes[7] = ViewUtil.findById(container, R.id.code_eighth);
|
||||
this.codes[8] = ViewUtil.findById(container, R.id.code_ninth);
|
||||
this.codes[9] = ViewUtil.findById(container, R.id.code_tenth);
|
||||
this.codes[10] = ViewUtil.findById(container, R.id.code_eleventh);
|
||||
this.codes[11] = ViewUtil.findById(container, R.id.code_twelth);
|
||||
|
||||
this.qrCodeContainer.setOnClickListener(clickListener);
|
||||
this.qrCode.setOnClickListener(clickListener);
|
||||
this.registerForContextMenu(numbersContainer);
|
||||
|
||||
updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false);
|
||||
this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true)));
|
||||
|
||||
this.scrollView.getViewTreeObserver().addOnScrollChangedListener(this);
|
||||
|
||||
((AppCompatActivity)requireActivity()).setSupportActionBar(toolbar);
|
||||
((AppCompatActivity)requireActivity()).setTitle(R.string.AndroidManifest__verify_safety_number);
|
||||
this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
|
||||
this.verified.setOnCheckedChangeListener(this);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override public void onDestroyView() {
|
||||
this.scrollView.getViewTreeObserver().removeOnScrollChangedListener(this);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
@@ -319,7 +306,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
byte[] localId;
|
||||
byte[] remoteId;
|
||||
|
||||
//noinspection WrongThread
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
|
||||
@@ -354,7 +340,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Fingerprint fingerprint) {
|
||||
if (getActivity() == null) return;
|
||||
VerifyDisplayFragment.this.fingerprint = fingerprint;
|
||||
setFingerprintViews(fingerprint, true);
|
||||
getActivity().supportInvalidateOptionsMenu();
|
||||
@@ -381,8 +366,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
animateFailureOnDraw = false;
|
||||
animateVerifiedFailure();
|
||||
}
|
||||
|
||||
ThreadUtil.postToMain(this::onScrollChanged);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -440,11 +423,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
this.animateFailureOnDraw = true;
|
||||
} catch (Exception e) {
|
||||
} catch (FingerprintParsingException e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
|
||||
this.animateFailureOnDraw = true;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,7 +495,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
private void setRecipientText(Recipient recipient) {
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
@@ -533,11 +516,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
if (animate) {
|
||||
ViewUtil.fadeIn(qrCode, 1000);
|
||||
ViewUtil.fadeIn(tapLabel, 1000);
|
||||
ViewUtil.fadeOut(loading, 300, View.GONE);
|
||||
} else {
|
||||
qrCode.setVisibility(View.VISIBLE);
|
||||
tapLabel.setVisibility(View.VISIBLE);
|
||||
loading.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,8 +574,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__successful_match));
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
@@ -605,8 +584,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__failed_to_verify_safety_number));
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
@@ -614,7 +591,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
scaleAnimation.setInterpolator(new FastOutSlowInInterpolator());
|
||||
scaleAnimation.setInterpolator(new OvershootInterpolator());
|
||||
scaleAnimation.setDuration(800);
|
||||
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
@@ -632,9 +609,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
scaleAnimation.setInterpolator(new AnticipateInterpolator());
|
||||
scaleAnimation.setDuration(500);
|
||||
ViewUtil.animateOut(qrVerified, scaleAnimation, View.GONE);
|
||||
ViewUtil.fadeIn(qrCode, 800);
|
||||
qrCodeContainer.setEnabled(true);
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__tap_to_scan));
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
@@ -643,70 +617,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
|
||||
ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE);
|
||||
ViewUtil.animateIn(qrVerified, scaleAnimation);
|
||||
qrCodeContainer.setEnabled(false);
|
||||
}
|
||||
|
||||
private void updateVerifyButton(boolean verified, boolean update) {
|
||||
currentVerifiedState = verified;
|
||||
|
||||
if (verified) {
|
||||
verifyButton.setText(R.string.verify_display_fragment__clear_verification);
|
||||
} else {
|
||||
verifyButton.setText(R.string.verify_display_fragment__mark_as_verified);
|
||||
}
|
||||
|
||||
if (update) {
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (verified) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
ApplicationDependencies.getIdentityStore()
|
||||
.saveIdentityWithoutSideEffects(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
true);
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
|
||||
new AsyncTask<Recipient, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Recipient... params) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
if (isChecked) {
|
||||
Log.i(TAG, "Saving identity: " + params[0].getId());
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.saveIdentity(params[0].getId(),
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED, false,
|
||||
System.currentTimeMillis(), true);
|
||||
} else {
|
||||
ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, VerifiedStatus.DEFAULT);
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.setVerified(params[0].getId(),
|
||||
remoteIdentity,
|
||||
VerifiedStatus.DEFAULT);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
|
||||
.add(new MultiDeviceVerifiedUpdateJob(recipient.getId(),
|
||||
remoteIdentity,
|
||||
verified ? VerifiedStatus.VERIFIED
|
||||
: VerifiedStatus.DEFAULT));
|
||||
isChecked ? VerifiedStatus.VERIFIED :
|
||||
VerifiedStatus.DEFAULT));
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), verified, false);
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), isChecked, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override public void onScrollChanged() {
|
||||
if (scrollView.canScrollVertically(-1)) {
|
||||
if (toolbarShadow.getVisibility() != View.VISIBLE) {
|
||||
ViewUtil.fadeIn(toolbarShadow, 250);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (toolbarShadow.getVisibility() != View.GONE) {
|
||||
ViewUtil.fadeOut(toolbarShadow, 250);
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollView.canScrollVertically(1)) {
|
||||
if (bottomShadow.getVisibility() != View.VISIBLE) {
|
||||
ViewUtil.fadeIn(bottomShadow, 250);
|
||||
}
|
||||
} else {
|
||||
ViewUtil.fadeOut(bottomShadow, 250);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,23 +659,12 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
private View container;
|
||||
private CameraView cameraView;
|
||||
private ShapeScrim cameraScrim;
|
||||
private ImageView cameraMarks;
|
||||
private ScanningThread scanningThread;
|
||||
private ScanListener scanListener;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = container.findViewById(R.id.scanner);
|
||||
this.cameraScrim = container.findViewById(R.id.camera_scrim);
|
||||
this.cameraMarks = container.findViewById(R.id.camera_marks);
|
||||
|
||||
OneShotPreDrawListener.add(cameraScrim, () -> {
|
||||
int width = cameraScrim.getScrimWidth();
|
||||
int height = cameraScrim.getScrimHeight();
|
||||
|
||||
ViewUtil.updateLayoutParams(cameraMarks, width, height);
|
||||
});
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = ViewUtil.findById(container, R.id.scanner);
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -767,4 +701,5 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,74 +18,53 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Rational;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.window.DisplayFeature;
|
||||
import androidx.window.FoldingFeature;
|
||||
import androidx.window.WindowLayoutInfo;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
|
||||
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
@@ -100,16 +79,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
|
||||
|
||||
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
|
||||
private DeviceOrientationMonitor deviceOrientationMonitor;
|
||||
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
private WebRtcCallView callScreen;
|
||||
private TooltipPopup videoTooltip;
|
||||
private WebRtcCallViewModel viewModel;
|
||||
private boolean enableVideoIfAvailable;
|
||||
private androidx.window.WindowManager windowManager;
|
||||
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
|
||||
private ThrottledDebouncer requestNewSizesThrottle;
|
||||
private WebRtcCallView callScreen;
|
||||
private TooltipPopup videoTooltip;
|
||||
private WebRtcCallViewModel viewModel;
|
||||
private boolean enableVideoIfAvailable;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
@@ -117,7 +91,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Log.i(TAG, "onCreate()");
|
||||
@@ -125,32 +98,20 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
boolean isLandscapeEnabled = getResources().getConfiguration().smallestScreenWidthDp >= 480;
|
||||
if (!isLandscapeEnabled) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.webrtc_call_activity);
|
||||
|
||||
fullscreenHelper = new FullscreenHelper(this);
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().hide();
|
||||
|
||||
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||
|
||||
initializeResources();
|
||||
initializeViewModel(isLandscapeEnabled);
|
||||
initializeViewModel();
|
||||
|
||||
processIntent(getIntent());
|
||||
|
||||
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
|
||||
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
|
||||
|
||||
windowManager = new androidx.window.WindowManager(this);
|
||||
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
|
||||
|
||||
windowManager.registerLayoutChangeCallback(SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
|
||||
|
||||
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -176,13 +137,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
Log.i(TAG, "onPause");
|
||||
super.onPause();
|
||||
|
||||
if (!isInPipMode() || isFinishing()) {
|
||||
if (!isInPipMode()) {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
if (!viewModel.isCallingStarted()) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -193,27 +154,18 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
Log.i(TAG, "onStop");
|
||||
super.onStop();
|
||||
|
||||
if (!isInPipMode() || isFinishing()) {
|
||||
EventBus.getDefault().unregister(this);
|
||||
requestNewSizesThrottle.clear();
|
||||
}
|
||||
EventBus.getDefault().unregister(this);
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
if (!viewModel.isCallingStarted()) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
windowManager.unregisterLayoutChangeCallback(windowLayoutInfoConsumer);
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
@@ -240,8 +192,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private boolean enterPipModeIfPossible() {
|
||||
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
|
||||
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(9, 16))
|
||||
.build();
|
||||
.setAspectRatio(new Rational(9, 16))
|
||||
.build();
|
||||
enterPictureInPictureMode(params);
|
||||
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
|
||||
|
||||
@@ -280,56 +232,38 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen);
|
||||
}
|
||||
|
||||
private void initializeViewModel(boolean isLandscapeEnabled) {
|
||||
deviceOrientationMonitor = new DeviceOrientationMonitor(this);
|
||||
getLifecycle().addObserver(deviceOrientationMonitor);
|
||||
|
||||
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class);
|
||||
viewModel.setIsLandscapeEnabled(isLandscapeEnabled);
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
|
||||
viewModel.setIsInPipMode(isInPipMode());
|
||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
|
||||
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
|
||||
viewModel.getOrientationAndLandscapeEnabled(),
|
||||
(s, o) -> new CallParticipantsViewState(s, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
|
||||
.observe(this, p -> callScreen.updateCallParticipants(p));
|
||||
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
|
||||
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
|
||||
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
|
||||
viewModel.getGroupMembersChanged().observe(this, unused -> updateGroupMembersForGroupCall());
|
||||
viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange);
|
||||
viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall());
|
||||
viewModel.shouldShowSpeakerHint().observe(this, this::updateSpeakerHint);
|
||||
|
||||
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null) {
|
||||
if (state.needsNewRequestSizes()) {
|
||||
requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions());
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
|
||||
viewModel.getControlsRotation().observe(this, callScreen::rotateControls);
|
||||
}
|
||||
|
||||
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
||||
if (event instanceof WebRtcCallViewModel.Event.StartCall) {
|
||||
startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall());
|
||||
startCall(((WebRtcCallViewModel.Event.StartCall)event).isVideoCall());
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
|
||||
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) {
|
||||
callScreen.switchToSpeakerView();
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.ShowSwipeToSpeakerHint) {
|
||||
CallToastPopupWindow.show(callScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInPipMode()) {
|
||||
@@ -367,19 +301,30 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void handleSetAudioHandset() {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.EARPIECE);
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_SPEAKER);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleSetAudioSpeaker() {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE);
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_SPEAKER);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_SPEAKER, true);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleSetAudioBluetooth() {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.BLUETOOTH);
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_BLUETOOTH);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_BLUETOOTH, true);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleSetMuteAudio(boolean enabled) {
|
||||
ApplicationDependencies.getSignalCallManager().setMuteAudio(enabled);
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_SET_MUTE_AUDIO);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_MUTE, enabled);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleSetMuteVideo(boolean muted) {
|
||||
@@ -393,13 +338,20 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName))
|
||||
.onAllGranted(() -> ApplicationDependencies.getSignalCallManager().setMuteVideo(!muted))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_SET_ENABLE_VIDEO);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_ENABLE, !muted);
|
||||
startService(intent);
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFlipCamera() {
|
||||
ApplicationDependencies.getSignalCallManager().flipCamera();
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_FLIP_CAMERA);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleAnswerWithAudio() {
|
||||
@@ -416,7 +368,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(false);
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
|
||||
startService(intent);
|
||||
})
|
||||
.onAnyDenied(this::handleDenyCall)
|
||||
.execute();
|
||||
@@ -431,13 +385,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
|
||||
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
||||
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(true);
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_ANSWER_WITH_VIDEO, true);
|
||||
startService(intent);
|
||||
|
||||
handleSetMuteVideo(false);
|
||||
})
|
||||
@@ -450,7 +407,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
Recipient recipient = viewModel.getRecipient().get();
|
||||
|
||||
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||
ApplicationDependencies.getSignalCallManager().denyCall();
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_DENY_CALL);
|
||||
startService(intent);
|
||||
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_ending_call));
|
||||
@@ -460,7 +419,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
private void handleEndCall() {
|
||||
Log.i(TAG, "Hangup pressed, handling termination now...");
|
||||
ApplicationDependencies.getSignalCallManager().localHangup();
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_LOCAL_HANGUP);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
||||
@@ -491,7 +452,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private void handleCallBusy() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
||||
delayedFinish(SignalCallManager.BUSY_TONE_LENGTH);
|
||||
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
|
||||
}
|
||||
|
||||
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
||||
@@ -510,18 +471,19 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private void handleServerFailure() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
|
||||
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.RedPhone_number_not_registered)
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.show();
|
||||
.setTitle(R.string.RedPhone_number_not_registered)
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
||||
@@ -529,7 +491,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
|
||||
|
||||
if (theirKey == null) {
|
||||
Log.w(TAG, "Untrusted identity without an identity key, terminating call.");
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
@@ -548,13 +509,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void updateGroupMembersForGroupCall() {
|
||||
ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers();
|
||||
}
|
||||
|
||||
public void handleGroupMemberCountChange(int count) {
|
||||
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize() && FeatureFlags.groupCallRinging();
|
||||
callScreen.enableRingGroup(canRing);
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
|
||||
startService(new Intent(this, WebRtcCallService.class).setAction(WebRtcCallService.ACTION_GROUP_REQUEST_UPDATE_MEMBERS));
|
||||
}
|
||||
|
||||
private void updateSpeakerHint(boolean showSpeakerHint) {
|
||||
@@ -568,15 +523,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange(@NonNull List<RecipientId> changedRecipients) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.getGroupCallState().isConnected()) {
|
||||
ApplicationDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients);
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_GROUP_APPROVE_SAFETY_CHANGE)
|
||||
.putExtra(WebRtcCallService.EXTRA_RECIPIENT_IDS, RecipientId.toSerializedList(changedRecipients));
|
||||
startService(intent);
|
||||
} else {
|
||||
viewModel.startCall(state.getLocalParticipant().isVideoEnabled());
|
||||
startCall(state.getLocalParticipant().isVideoEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,8 +540,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
public void onCanceled() {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null && state.getGroupCallState().isNotIdle()) {
|
||||
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
|
||||
startService(intent);
|
||||
finish();
|
||||
} else {
|
||||
handleEndCall();
|
||||
@@ -619,34 +574,20 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
|
||||
switch (event.getState()) {
|
||||
case CALL_PRE_JOIN:
|
||||
handleCallPreJoin(event); break;
|
||||
case CALL_CONNECTED:
|
||||
handleCallConnected(event); break;
|
||||
case NETWORK_FAILURE:
|
||||
handleServerFailure(); break;
|
||||
case CALL_RINGING:
|
||||
handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||
case CALL_ACCEPTED_ELSEWHERE:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||
case CALL_DECLINED_ELSEWHERE:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
|
||||
case CALL_ONGOING_ELSEWHERE:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||
case CALL_NEEDS_PERMISSION:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||
case NO_SUCH_USER:
|
||||
handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE:
|
||||
handleRecipientUnavailable(); break;
|
||||
case CALL_OUTGOING:
|
||||
handleOutgoingCall(event); break;
|
||||
case CALL_BUSY:
|
||||
handleCallBusy(); break;
|
||||
case UNTRUSTED_IDENTITY:
|
||||
handleUntrustedIdentity(event); break;
|
||||
case CALL_PRE_JOIN: handleCallPreJoin(event); break;
|
||||
case CALL_CONNECTED: handleCallConnected(event); break;
|
||||
case NETWORK_FAILURE: handleServerFailure(); break;
|
||||
case CALL_RINGING: handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
|
||||
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
||||
case CALL_BUSY: handleCallBusy(); break;
|
||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||
}
|
||||
|
||||
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
@@ -662,22 +603,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
|
||||
if (event.getGroupState().isNotIdle()) {
|
||||
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||
callScreen.setRingGroup(event.shouldRingGroup());
|
||||
|
||||
if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startCall(boolean isVideoCall) {
|
||||
enableVideoIfAvailable = isVideoCall;
|
||||
|
||||
if (isVideoCall) {
|
||||
ApplicationDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get());
|
||||
} else {
|
||||
ApplicationDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get());
|
||||
}
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
|
||||
startService(intent);
|
||||
|
||||
MessageSender.onMessageSent();
|
||||
}
|
||||
@@ -701,16 +637,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSystemUI() {
|
||||
fullscreenHelper.showSystemUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideSystemUI() {
|
||||
fullscreenHelper.hideSystemUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
|
||||
switch (audioOutput) {
|
||||
@@ -776,43 +702,5 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
|
||||
viewModel.setIsViewingFocusedParticipant(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocalPictureInPictureClicked() {
|
||||
viewModel.onLocalPictureInPictureClicked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) {
|
||||
if (ringingAllowed) {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(ringGroup);
|
||||
} else {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
|
||||
Toast.makeText(WebRtcCallActivity.this, R.string.WebRtcCallActivity__group_is_too_large_to_ring_the_participants, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {
|
||||
|
||||
@Override
|
||||
public void accept(WindowLayoutInfo windowLayoutInfo) {
|
||||
Log.d(TAG, "On WindowLayoutInfo accepted: " + windowLayoutInfo.toString());
|
||||
|
||||
Optional<DisplayFeature> feature = windowLayoutInfo.getDisplayFeatures().stream().filter(f -> f instanceof FoldingFeature).findFirst();
|
||||
viewModel.setIsLandscapeEnabled(feature.isPresent());
|
||||
setRequestedOrientation(feature.isPresent() ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
if (feature.isPresent()) {
|
||||
FoldingFeature foldingFeature = (FoldingFeature) feature.get();
|
||||
Rect bounds = foldingFeature.getBounds();
|
||||
if (foldingFeature.getState() == FoldingFeature.State.HALF_OPENED && bounds.top == bounds.bottom) {
|
||||
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode");
|
||||
viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top));
|
||||
} else {
|
||||
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in flat display mode");
|
||||
viewModel.setFoldableState(WebRtcControls.FoldableState.flat());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionValues
|
||||
|
||||
private const val ALPHA = "signal.alpha_transition.alpha"
|
||||
|
||||
/**
|
||||
* Alpha transition that can be used with [ConstraintLayout]
|
||||
*/
|
||||
class AlphaTransition : Transition() {
|
||||
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
if (view !is ConstraintLayout) {
|
||||
transitionValues.values[ALPHA] = view.alpha
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
val startAlpha: Float = startValues.values[ALPHA] as? Float ?: view.alpha
|
||||
val endAlpha: Float = endValues.values[ALPHA] as? Float ?: view.alpha
|
||||
|
||||
return ObjectAnimator.ofFloat(view, "alpha", startAlpha, endAlpha)
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.animation.TypeEvaluator
|
||||
import android.content.Context
|
||||
import android.transition.Transition
|
||||
import android.transition.TransitionValues
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
|
||||
private const val WIDTH = "signal.circleavatartransition.width"
|
||||
private const val HEIGHT = "signal.circleavatartransition.height"
|
||||
|
||||
/**
|
||||
* Custom transition for Circular avatars, because once you have multiple things animating stuff was getting broken and weird.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
|
||||
if (view.transitionName == "avatar") {
|
||||
val topLeft = intArrayOf(0, 0)
|
||||
view.getLocationOnScreen(topLeft)
|
||||
transitionValues.values[POSITION_ON_SCREEN] = topLeft
|
||||
transitionValues.values[WIDTH] = view.measuredWidth
|
||||
transitionValues.values[HEIGHT] = view.measuredHeight
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
if (view.transitionName != "avatar") {
|
||||
return null
|
||||
}
|
||||
|
||||
val startCoords: IntArray = startValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
|
||||
val endCoords: IntArray = endValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
|
||||
|
||||
val startWidth: Int = startValues.values[WIDTH] as? Int ?: view.measuredWidth
|
||||
val endWidth: Int = endValues.values[WIDTH] as? Int ?: view.measuredWidth
|
||||
|
||||
val startHeight: Int = startValues.values[HEIGHT] as? Int ?: view.measuredHeight
|
||||
val endHeight: Int = endValues.values[HEIGHT] as? Int ?: view.measuredHeight
|
||||
|
||||
val startHeightOffset = (endHeight - startHeight) / 2f
|
||||
val startWidthOffset = (endWidth - startWidth) / 2f
|
||||
|
||||
val translateXHolder = PropertyValuesHolder.ofFloat("translationX", startCoords[0] - endCoords[0] - startWidthOffset, 0f).apply {
|
||||
setEvaluator(FloatInterpolatorEvaluator(DecelerateInterpolator()))
|
||||
}
|
||||
val translateYHolder = PropertyValuesHolder.ofFloat("translationY", startCoords[1] - endCoords[1] - startHeightOffset, 0f).apply {
|
||||
setEvaluator(FloatInterpolatorEvaluator(AccelerateInterpolator()))
|
||||
}
|
||||
|
||||
val widthRatio = startWidth.toFloat() / endWidth
|
||||
val scaleXHolder = PropertyValuesHolder.ofFloat("scaleX", widthRatio, 1f)
|
||||
|
||||
val heightRatio = startHeight.toFloat() / endHeight
|
||||
val scaleYHolder = PropertyValuesHolder.ofFloat("scaleY", heightRatio, 1f)
|
||||
|
||||
return ObjectAnimator.ofPropertyValuesHolder(view, translateXHolder, translateYHolder, scaleXHolder, scaleYHolder)
|
||||
}
|
||||
|
||||
private class FloatInterpolatorEvaluator(
|
||||
private val interpolator: Interpolator
|
||||
) : TypeEvaluator<Float> {
|
||||
|
||||
override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
|
||||
val interpolatedFraction = interpolator.getInterpolation(fraction)
|
||||
val delta = endValue - startValue
|
||||
|
||||
return delta * interpolatedFraction + startValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.RectEvaluator
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.transition.Transition
|
||||
import android.transition.TransitionValues
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
|
||||
private const val BOUNDS = "signal.wipedowntransition.bottom"
|
||||
|
||||
/**
|
||||
* WipeDownTransition will animate the bottom position of a view such that it "wipes" down the screen to a final position.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
class WipeDownTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
|
||||
if (view is ViewGroup) {
|
||||
val rect = Rect()
|
||||
view.getLocalVisibleRect(rect)
|
||||
transitionValues.values[BOUNDS] = rect
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
if (view !is FragmentContainerView) {
|
||||
return null
|
||||
}
|
||||
|
||||
val startBottom: Rect = startValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
|
||||
val endBottom: Rect = endValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
|
||||
|
||||
return ObjectAnimator.ofObject(view, "clipBounds", RectEvaluator(), startBottom, endBottom).apply {
|
||||
addListener(
|
||||
onEnd = {
|
||||
view.clipBounds = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ public abstract class Attachment {
|
||||
|
||||
private final boolean voiceNote;
|
||||
private final boolean borderless;
|
||||
private final boolean videoGif;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final boolean quote;
|
||||
@@ -73,7 +72,6 @@ public abstract class Attachment {
|
||||
@Nullable String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean videoGif,
|
||||
int width,
|
||||
int height,
|
||||
boolean quote,
|
||||
@@ -96,7 +94,6 @@ public abstract class Attachment {
|
||||
this.fastPreflightId = fastPreflightId;
|
||||
this.voiceNote = voiceNote;
|
||||
this.borderless = borderless;
|
||||
this.videoGif = videoGif;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.quote = quote;
|
||||
@@ -111,8 +108,6 @@ public abstract class Attachment {
|
||||
@Nullable
|
||||
public abstract Uri getUri();
|
||||
|
||||
public abstract @Nullable Uri getPublicUri();
|
||||
|
||||
public int getTransferState() {
|
||||
return transferState;
|
||||
}
|
||||
@@ -173,10 +168,6 @@ public abstract class Attachment {
|
||||
return borderless;
|
||||
}
|
||||
|
||||
public boolean isVideoGif() {
|
||||
return videoGif;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ public class DatabaseAttachment extends Attachment {
|
||||
String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean videoGif,
|
||||
int width,
|
||||
int height,
|
||||
boolean quote,
|
||||
@@ -48,7 +47,7 @@ public class DatabaseAttachment extends Attachment {
|
||||
int displayOrder,
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.attachmentId = attachmentId;
|
||||
this.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
@@ -66,15 +65,6 @@ public class DatabaseAttachment extends Attachment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getPublicUri() {
|
||||
if (hasData) {
|
||||
return PartAuthority.getAttachmentPublicUri(getUri());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public AttachmentId getAttachmentId() {
|
||||
return attachmentId;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
public class MmsNotificationAttachment extends Attachment {
|
||||
|
||||
public MmsNotificationAttachment(int status, long size) {
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null);
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, 0, 0, false, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -20,11 +20,6 @@ public class MmsNotificationAttachment extends Attachment {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getPublicUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int getTransferStateFromStatus(int status) {
|
||||
if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED ||
|
||||
status == MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY)
|
||||
|
||||
@@ -30,7 +30,6 @@ public class PointerAttachment extends Attachment {
|
||||
@Nullable String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean videoGif,
|
||||
int width,
|
||||
int height,
|
||||
long uploadTimestamp,
|
||||
@@ -38,7 +37,7 @@ public class PointerAttachment extends Attachment {
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -47,11 +46,6 @@ public class PointerAttachment extends Attachment {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getPublicUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
|
||||
List<Attachment> results = new LinkedList<>();
|
||||
|
||||
@@ -112,7 +106,6 @@ public class PointerAttachment extends Attachment {
|
||||
fastPreflightId,
|
||||
pointer.get().asPointer().getVoiceNote(),
|
||||
pointer.get().asPointer().isBorderless(),
|
||||
pointer.get().asPointer().isGif(),
|
||||
pointer.get().asPointer().getWidth(),
|
||||
pointer.get().asPointer().getHeight(),
|
||||
pointer.get().asPointer().getUploadTimestamp(),
|
||||
@@ -137,7 +130,6 @@ public class PointerAttachment extends Attachment {
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
|
||||
|
||||
@@ -16,16 +16,11 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
public class TombstoneAttachment extends Attachment {
|
||||
|
||||
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getPublicUri() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ public class UriAttachment extends Attachment {
|
||||
@Nullable String fileName,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean videoGif,
|
||||
boolean quote,
|
||||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@@ -29,7 +28,7 @@ public class UriAttachment extends Attachment {
|
||||
@Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
{
|
||||
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, videoGif, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
}
|
||||
|
||||
public UriAttachment(@NonNull Uri dataUri,
|
||||
@@ -42,7 +41,6 @@ public class UriAttachment extends Attachment {
|
||||
@Nullable String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean videoGif,
|
||||
boolean quote,
|
||||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@@ -50,7 +48,7 @@ public class UriAttachment extends Attachment {
|
||||
@Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.dataUri = dataUri;
|
||||
}
|
||||
|
||||
@@ -60,11 +58,6 @@ public class UriAttachment extends Attachment {
|
||||
return dataUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getPublicUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.nio.ByteBuffer;
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class AudioCodec {
|
||||
|
||||
private static final String TAG = Log.tag(AudioCodec.class);
|
||||
private static final String TAG = AudioCodec.class.getSimpleName();
|
||||
|
||||
private static final int SAMPLE_RATE = 44100;
|
||||
private static final int SAMPLE_RATE_INDEX = 4;
|
||||
@@ -32,7 +32,6 @@ public class AudioCodec {
|
||||
private final AudioRecord audioRecord;
|
||||
|
||||
private boolean running = true;
|
||||
private boolean failed = false;
|
||||
private boolean finished = false;
|
||||
|
||||
public AudioCodec() throws IOException {
|
||||
@@ -77,25 +76,10 @@ public class AudioCodec {
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
} finally {
|
||||
mediaCodec.stop();
|
||||
audioRecord.stop();
|
||||
|
||||
try {
|
||||
mediaCodec.stop();
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.w(TAG, "mediaCodec stop failed.", ise);
|
||||
}
|
||||
|
||||
try {
|
||||
audioRecord.stop();
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.w(TAG, "audioRecord stop failed.", ise);
|
||||
}
|
||||
|
||||
try {
|
||||
mediaCodec.release();
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.w(TAG, "mediaCodec release failed. Probably already released.", ise);
|
||||
}
|
||||
|
||||
mediaCodec.release();
|
||||
audioRecord.release();
|
||||
|
||||
StreamUtil.close(outputStream);
|
||||
|
||||
@@ -8,14 +8,14 @@ import android.os.ParcelFileDescriptor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -23,7 +23,7 @@ import java.util.concurrent.ExecutorService;
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class AudioRecorder {
|
||||
|
||||
private static final String TAG = Log.tag(AudioRecorder.class);
|
||||
private static final String TAG = AudioRecorder.class.getSimpleName();
|
||||
|
||||
private static final ExecutorService executor = SignalExecutors.newCachedSingleThreadExecutor("signal-AudioRecorder");
|
||||
|
||||
@@ -51,7 +51,7 @@ public class AudioRecorder {
|
||||
captureUri = BlobProvider.getInstance()
|
||||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||
.withMimeType(MediaUtil.AUDIO_AAC)
|
||||
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
||||
.createForSingleSessionOnDiskAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
||||
audioCodec = new AudioCodec();
|
||||
|
||||
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
|
||||
@@ -61,10 +61,10 @@ public class AudioRecorder {
|
||||
});
|
||||
}
|
||||
|
||||
public @NonNull ListenableFuture<VoiceNoteDraft> stopRecording() {
|
||||
public @NonNull ListenableFuture<Pair<Uri, Long>> stopRecording() {
|
||||
Log.i(TAG, "stopRecording()");
|
||||
|
||||
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
|
||||
final SettableFuture<Pair<Uri, Long>> future = new SettableFuture<>();
|
||||
|
||||
executor.execute(() -> {
|
||||
if (audioCodec == null) {
|
||||
@@ -76,7 +76,7 @@ public class AudioRecorder {
|
||||
|
||||
try {
|
||||
long size = MediaUtil.getMediaSize(context, captureUri);
|
||||
sendToFuture(future, new VoiceNoteDraft(captureUri, size));
|
||||
sendToFuture(future, new Pair<>(captureUri, size));
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
sendToFuture(future, ioe);
|
||||
@@ -90,10 +90,10 @@ public class AudioRecorder {
|
||||
}
|
||||
|
||||
private <T> void sendToFuture(final SettableFuture<T> future, final Exception exception) {
|
||||
ThreadUtil.runOnMain(() -> future.setException(exception));
|
||||
Util.runOnMain(() -> future.setException(exception));
|
||||
}
|
||||
|
||||
private <T> void sendToFuture(final SettableFuture<T> future, final T result) {
|
||||
ThreadUtil.runOnMain(() -> future.set(result));
|
||||
Util.runOnMain(() -> future.set(result));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.core.util.Consumer;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
@@ -27,6 +26,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormDat
|
||||
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
||||
import org.thoughtcrime.securesms.media.MediaInput;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -61,7 +61,13 @@ public final class AudioWaveForm {
|
||||
|
||||
if (uri == null) {
|
||||
Log.w(TAG, "No uri");
|
||||
ThreadUtil.runOnMain(onFailure);
|
||||
Util.runOnMain(onFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(attachment instanceof DatabaseAttachment)) {
|
||||
Log.i(TAG, "Not yet in database");
|
||||
Util.runOnMain(onFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,7 +75,7 @@ public final class AudioWaveForm {
|
||||
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
|
||||
if (cached != null) {
|
||||
Log.i(TAG, "Loaded wave form from cache " + cacheKey);
|
||||
ThreadUtil.runOnMain(() -> onSuccess.accept(cached));
|
||||
Util.runOnMain(() -> onSuccess.accept(cached));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +83,7 @@ public final class AudioWaveForm {
|
||||
AudioFileInfo cachedInExecutor = WAVE_FORM_CACHE.get(cacheKey);
|
||||
if (cachedInExecutor != null) {
|
||||
Log.i(TAG, "Loaded wave form from cache inside executor" + cacheKey);
|
||||
ThreadUtil.runOnMain(() -> onSuccess.accept(cachedInExecutor));
|
||||
Util.runOnMain(() -> onSuccess.accept(cachedInExecutor));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,58 +92,38 @@ public final class AudioWaveForm {
|
||||
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
|
||||
if (audioFileInfo.waveForm.length == 0) {
|
||||
Log.w(TAG, "Recovering from a wave form generation error " + cacheKey);
|
||||
ThreadUtil.runOnMain(onFailure);
|
||||
Util.runOnMain(onFailure);
|
||||
return;
|
||||
} else if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
Log.w(TAG, "Wave form from database does not match bar count, regenerating " + cacheKey);
|
||||
} else {
|
||||
WAVE_FORM_CACHE.put(cacheKey, audioFileInfo);
|
||||
Log.i(TAG, "Loaded wave form from DB " + cacheKey);
|
||||
ThreadUtil.runOnMain(() -> onSuccess.accept(audioFileInfo));
|
||||
Util.runOnMain(() -> onSuccess.accept(audioFileInfo));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (attachment instanceof DatabaseAttachment) {
|
||||
try {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
|
||||
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
} catch (Throwable e) {
|
||||
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||
ThreadUtil.runOnMain(onFailure);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Log.i(TAG, "Not in database and not cached. Generating wave form on-the-fly.");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||
ThreadUtil.runOnMain(onFailure);
|
||||
}
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
Util.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
} catch (Throwable e) {
|
||||
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||
Util.runOnMain(onFailure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Represents an Avatar which the user can choose, edit, and render into a bitmap via the renderer.
|
||||
*/
|
||||
sealed class Avatar(
|
||||
open val databaseId: DatabaseId
|
||||
) {
|
||||
data class Resource(
|
||||
val resourceId: Int,
|
||||
val color: Avatars.ColorPair
|
||||
) : Avatar(DatabaseId.DoNotPersist) {
|
||||
override fun isSameAs(other: Avatar): Boolean {
|
||||
return other is Resource && other.resourceId == resourceId
|
||||
}
|
||||
}
|
||||
|
||||
data class Text(
|
||||
val text: String,
|
||||
val color: Avatars.ColorPair,
|
||||
override val databaseId: DatabaseId,
|
||||
) : Avatar(databaseId) {
|
||||
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
|
||||
return copy(databaseId = databaseId)
|
||||
}
|
||||
|
||||
override fun isSameAs(other: Avatar): Boolean {
|
||||
return other is Text && other.databaseId == databaseId
|
||||
}
|
||||
}
|
||||
|
||||
data class Vector(
|
||||
val key: String,
|
||||
val color: Avatars.ColorPair,
|
||||
override val databaseId: DatabaseId,
|
||||
) : Avatar(databaseId) {
|
||||
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
|
||||
return copy(databaseId = databaseId)
|
||||
}
|
||||
|
||||
override fun isSameAs(other: Avatar): Boolean {
|
||||
return other is Vector && other.key == key
|
||||
}
|
||||
}
|
||||
|
||||
data class Photo(
|
||||
val uri: Uri,
|
||||
val size: Long,
|
||||
override val databaseId: DatabaseId
|
||||
) : Avatar(databaseId) {
|
||||
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
|
||||
return copy(databaseId = databaseId)
|
||||
}
|
||||
|
||||
override fun isSameAs(other: Avatar): Boolean {
|
||||
return other is Photo && databaseId == other.databaseId
|
||||
}
|
||||
}
|
||||
|
||||
open fun withDatabaseId(databaseId: DatabaseId): Avatar {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
abstract fun isSameAs(other: Avatar): Boolean
|
||||
|
||||
companion object {
|
||||
fun getDefaultForSelf(): Resource = Resource(R.drawable.ic_profile_outline_40, Avatars.colors.random())
|
||||
fun getDefaultForGroup(): Resource = Resource(R.drawable.ic_group_outline_40, Avatars.colors.random())
|
||||
}
|
||||
|
||||
sealed class DatabaseId {
|
||||
object DoNotPersist : DatabaseId()
|
||||
object NotSet : DatabaseId()
|
||||
data class Saved(val id: Long) : DatabaseId()
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.os.Bundle
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
/**
|
||||
* Utility class which encapsulates reading and writing Avatar objects to and from Bundles.
|
||||
*/
|
||||
object AvatarBundler {
|
||||
|
||||
private const val TEXT = "org.thoughtcrime.securesms.avatar.TEXT"
|
||||
private const val COLOR = "org.thoughtcrime.securesms.avatar.COLOR"
|
||||
private const val URI = "org.thoughtcrime.securesms.avatar.URI"
|
||||
private const val KEY = "org.thoughtcrime.securesms.avatar.KEY"
|
||||
private const val DATABASE_ID = "org.thoughtcrime.securesms.avatar.DATABASE_ID"
|
||||
private const val SIZE = "org.thoughtcrime.securesms.avatar.SIZE"
|
||||
|
||||
fun bundleText(text: Avatar.Text): Bundle = Bundle().apply {
|
||||
putString(TEXT, text.text)
|
||||
putString(COLOR, text.color.code)
|
||||
putDatabaseId(DATABASE_ID, text.databaseId)
|
||||
}
|
||||
|
||||
fun extractText(bundle: Bundle): Avatar.Text = Avatar.Text(
|
||||
text = requireNotNull(bundle.getString(TEXT)),
|
||||
color = Avatars.colorMap[bundle.getString(COLOR)] ?: throw IllegalStateException(),
|
||||
databaseId = bundle.getDatabaseId()
|
||||
)
|
||||
|
||||
fun bundlePhoto(photo: Avatar.Photo): Bundle = Bundle().apply {
|
||||
putParcelable(URI, photo.uri)
|
||||
putLong(SIZE, photo.size)
|
||||
putDatabaseId(DATABASE_ID, photo.databaseId)
|
||||
}
|
||||
|
||||
fun extractPhoto(bundle: Bundle): Avatar.Photo = Avatar.Photo(
|
||||
uri = requireNotNull(bundle.getParcelable(URI)),
|
||||
size = bundle.getLong(SIZE),
|
||||
databaseId = bundle.getDatabaseId()
|
||||
)
|
||||
|
||||
fun bundleVector(vector: Avatar.Vector): Bundle = Bundle().apply {
|
||||
putString(KEY, vector.key)
|
||||
putString(COLOR, vector.color.code)
|
||||
putDatabaseId(DATABASE_ID, vector.databaseId)
|
||||
}
|
||||
|
||||
fun extractVector(bundle: Bundle): Avatar.Vector = Avatar.Vector(
|
||||
key = requireNotNull(bundle.getString(KEY)),
|
||||
color = Avatars.colorMap[bundle.getString(COLOR)] ?: throw IllegalStateException(),
|
||||
databaseId = bundle.getDatabaseId()
|
||||
)
|
||||
|
||||
private fun Bundle.getDatabaseId(): Avatar.DatabaseId {
|
||||
val id = getLong(DATABASE_ID, -1L)
|
||||
|
||||
return if (id == -1L) {
|
||||
Avatar.DatabaseId.NotSet
|
||||
} else {
|
||||
Avatar.DatabaseId.Saved(id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bundle.putDatabaseId(key: String, databaseId: Avatar.DatabaseId) {
|
||||
if (databaseId is Avatar.DatabaseId.Saved) {
|
||||
putLong(key, databaseId.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
typealias OnAvatarColorClickListener = (Avatars.ColorPair) -> Unit
|
||||
|
||||
/**
|
||||
* Selectable color item for choosing colors when editing a Text or Vector avatar.
|
||||
*/
|
||||
data class AvatarColorItem(
|
||||
val colors: Avatars.ColorPair,
|
||||
val selected: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun registerViewHolder(adapter: MappingAdapter, onAvatarColorClickListener: OnAvatarColorClickListener) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onAvatarColorClickListener) }, R.layout.avatar_color_item))
|
||||
}
|
||||
}
|
||||
|
||||
class Model(val colorItem: AvatarColorItem) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = newItem.colorItem.colors == colorItem.colors
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = newItem.colorItem == colorItem
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View, private val onAvatarColorClickListener: OnAvatarColorClickListener) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val imageView: ImageView = findViewById(R.id.avatar_color_item)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.setOnClickListener { onAvatarColorClickListener(model.colorItem.colors) }
|
||||
imageView.background.colorFilter = SimpleColorFilter(model.colorItem.colors.backgroundColor)
|
||||
imageView.isSelected = model.colorItem.selected
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.storage.FileStorage
|
||||
import java.io.InputStream
|
||||
|
||||
object AvatarPickerStorage {
|
||||
|
||||
private const val DIRECTORY = "avatar_picker"
|
||||
private const val FILENAME_BASE = "avatar"
|
||||
|
||||
@JvmStatic
|
||||
fun read(context: Context, fileName: String) = FileStorage.read(context, DIRECTORY, fileName)
|
||||
|
||||
fun save(context: Context, media: Media): Uri {
|
||||
val fileName = FileStorage.save(context, PartAuthority.getAttachmentStream(context, media.uri), DIRECTORY, FILENAME_BASE, MediaUtil.getExtension(context, media.uri) ?: "")
|
||||
|
||||
return PartAuthority.getAvatarPickerUri(fileName)
|
||||
}
|
||||
|
||||
fun save(context: Context, inputStream: InputStream): Uri {
|
||||
val fileName = FileStorage.save(context, inputStream, DIRECTORY, FILENAME_BASE, MimeTypeMap.getSingleton().getExtensionFromMimeType(MediaUtil.IMAGE_JPEG) ?: "")
|
||||
|
||||
return PartAuthority.getAvatarPickerUri(fileName)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun cleanOrphans(context: Context) {
|
||||
val avatarFiles = FileStorage.getAllFiles(context, DIRECTORY, FILENAME_BASE)
|
||||
val database = DatabaseFactory.getAvatarPickerDatabase(context)
|
||||
val photoAvatars = database
|
||||
.getAllAvatars()
|
||||
.filterIsInstance<Avatar.Photo>()
|
||||
|
||||
val inDatabaseFileNames = photoAvatars.map { PartAuthority.getAvatarPickerFilename(it.uri) }
|
||||
val onDiskFileNames = avatarFiles.map { it.name }
|
||||
|
||||
val inDatabaseButNotOnDisk = inDatabaseFileNames - onDiskFileNames
|
||||
val onDiskButNotInDatabase = onDiskFileNames - inDatabaseFileNames
|
||||
|
||||
avatarFiles
|
||||
.filter { onDiskButNotInDatabase.contains(it.name) }
|
||||
.forEach { it.delete() }
|
||||
|
||||
photoAvatars
|
||||
.filter { inDatabaseButNotOnDisk.contains(PartAuthority.getAvatarPickerFilename(it.uri)) }
|
||||
.forEach { database.deleteAvatar(it) }
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import javax.annotation.meta.Exhaustive
|
||||
|
||||
/**
|
||||
* Renders Avatar objects into Media objects. This can involve creating a Bitmap, depending on the
|
||||
* type of Avatar passed to `renderAvatar`
|
||||
*/
|
||||
object AvatarRenderer {
|
||||
|
||||
val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS
|
||||
|
||||
fun getTypeface(context: Context): Typeface {
|
||||
return Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
|
||||
}
|
||||
|
||||
fun renderAvatar(context: Context, avatar: Avatar, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
@Exhaustive
|
||||
when (avatar) {
|
||||
is Avatar.Resource -> renderResource(context, avatar, onAvatarRendered, onRenderFailed)
|
||||
is Avatar.Vector -> renderVector(context, avatar, onAvatarRendered, onRenderFailed)
|
||||
is Avatar.Photo -> renderPhoto(context, avatar, onAvatarRendered)
|
||||
is Avatar.Text -> renderText(context, avatar, onAvatarRendered, onRenderFailed)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createTextDrawable(
|
||||
context: Context,
|
||||
avatar: Avatar.Text,
|
||||
inverted: Boolean = false,
|
||||
size: Int = DIMENSIONS,
|
||||
synchronous: Boolean = false
|
||||
): Drawable {
|
||||
return TextAvatarDrawable(context, avatar, inverted, size, synchronous)
|
||||
}
|
||||
|
||||
private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
|
||||
val drawableResourceId = Avatars.getDrawableResource(avatar.key) ?: return@renderInBackground Result.failure(Exception("Drawable resource for key ${avatar.key} does not exist."))
|
||||
val vector: Drawable = requireNotNull(AppCompatResources.getDrawable(context, drawableResourceId))
|
||||
vector.setBounds(0, 0, DIMENSIONS, DIMENSIONS)
|
||||
|
||||
canvas.drawColor(avatar.color.backgroundColor)
|
||||
vector.draw(canvas)
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderText(context: Context, avatar: Avatar.Text, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
|
||||
val textDrawable = createTextDrawable(context, avatar, synchronous = true)
|
||||
|
||||
canvas.drawColor(avatar.color.backgroundColor)
|
||||
textDrawable.draw(canvas)
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderPhoto(context: Context, avatar: Avatar.Photo, onAvatarRendered: (Media) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val blob = BlobProvider.getInstance()
|
||||
.forData(AvatarPickerStorage.read(context, PartAuthority.getAvatarPickerFilename(avatar.uri)), avatar.size)
|
||||
.createForSingleSessionOnDisk(context)
|
||||
|
||||
onAvatarRendered(createMedia(blob, avatar.size))
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderResource(context: Context, avatar: Avatar.Resource, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
|
||||
val resource: Drawable = requireNotNull(AppCompatResources.getDrawable(context, avatar.resourceId))
|
||||
resource.colorFilter = SimpleColorFilter(avatar.color.foregroundColor)
|
||||
|
||||
val padding = (DIMENSIONS * 0.2).toInt()
|
||||
resource.setBounds(0 + padding, 0 + padding, DIMENSIONS - padding, DIMENSIONS - padding)
|
||||
|
||||
canvas.drawColor(avatar.color.backgroundColor)
|
||||
resource.draw(canvas)
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderInBackground(context: Context, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit, drawAvatar: (Canvas) -> Result<Unit>) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val canvasBitmap = Bitmap.createBitmap(DIMENSIONS, DIMENSIONS, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(canvasBitmap)
|
||||
|
||||
val drawResult = drawAvatar(canvas)
|
||||
if (drawResult.isFailure) {
|
||||
canvasBitmap.recycle()
|
||||
onRenderFailed(drawResult.exceptionOrNull())
|
||||
}
|
||||
|
||||
val outStream = ByteArrayOutputStream()
|
||||
val compressed = canvasBitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream)
|
||||
canvasBitmap.recycle()
|
||||
|
||||
if (!compressed) {
|
||||
onRenderFailed(IOException("Failed to compress bitmap"))
|
||||
return@execute
|
||||
}
|
||||
|
||||
val bytes = outStream.toByteArray()
|
||||
val inStream = ByteArrayInputStream(bytes)
|
||||
val uri = BlobProvider.getInstance().forData(inStream, bytes.size.toLong()).createForSingleSessionOnDisk(context)
|
||||
|
||||
onAvatarRendered(createMedia(uri, bytes.size.toLong()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMedia(uri: Uri, size: Long): Media {
|
||||
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent())
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Paint
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.Px
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
object Avatars {
|
||||
|
||||
/**
|
||||
* Enum class mirroring AvatarColors codes but utilizing foreground colors for text or icon tinting.
|
||||
*/
|
||||
enum class ForegroundColor(private val code: String, @ColorInt val colorInt: Int) {
|
||||
A100("A100", 0xFF3838F5.toInt()),
|
||||
A110("A110", 0xFF1251D3.toInt()),
|
||||
A120("A120", 0xFF086DA0.toInt()),
|
||||
A130("A130", 0xFF067906.toInt()),
|
||||
A140("A140", 0xFF661AFF.toInt()),
|
||||
A150("A150", 0xFF9F00F0.toInt()),
|
||||
A160("A160", 0xFFB8057C.toInt()),
|
||||
A170("A170", 0xFFBE0404.toInt()),
|
||||
A180("A180", 0xFF836B01.toInt()),
|
||||
A190("A190", 0xFF7D6F40.toInt()),
|
||||
A200("A200", 0xFF4F4F6D.toInt()),
|
||||
A210("A210", 0xFF5C5C5C.toInt());
|
||||
|
||||
fun deserialize(code: String): ForegroundColor {
|
||||
return values().find { it.code == code } ?: throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
fun serialize(): String = code
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping which associates color codes to ColorPair objects containing background and foreground colors.
|
||||
*/
|
||||
val colorMap: Map<String, ColorPair> = ForegroundColor.values().map {
|
||||
ColorPair(AvatarColor.deserialize(it.serialize()), it)
|
||||
}.associateBy {
|
||||
it.code
|
||||
}
|
||||
|
||||
val colors: List<ColorPair> = colorMap.values.toList()
|
||||
|
||||
val defaultAvatarsForSelf = linkedMapOf(
|
||||
"avatar_abstract_01" to DefaultAvatar(R.drawable.ic_avatar_abstract_01, "A130"),
|
||||
"avatar_abstract_02" to DefaultAvatar(R.drawable.ic_avatar_abstract_02, "A120"),
|
||||
"avatar_abstract_03" to DefaultAvatar(R.drawable.ic_avatar_abstract_03, "A170"),
|
||||
"avatar_cat" to DefaultAvatar(R.drawable.ic_avatar_cat, "A190"),
|
||||
"avatar_dog" to DefaultAvatar(R.drawable.ic_avatar_dog, "A140"),
|
||||
"avatar_fox" to DefaultAvatar(R.drawable.ic_avatar_fox, "A190"),
|
||||
"avatar_tucan" to DefaultAvatar(R.drawable.ic_avatar_tucan, "A120"),
|
||||
"avatar_sloth" to DefaultAvatar(R.drawable.ic_avatar_sloth, "A160"),
|
||||
"avatar_dinosaur" to DefaultAvatar(R.drawable.ic_avatar_dinosour, "A130"),
|
||||
"avatar_pig" to DefaultAvatar(R.drawable.ic_avatar_pig, "A180"),
|
||||
"avatar_incognito" to DefaultAvatar(R.drawable.ic_avatar_incognito, "A220"),
|
||||
"avatar_ghost" to DefaultAvatar(R.drawable.ic_avatar_ghost, "A100")
|
||||
)
|
||||
|
||||
val defaultAvatarsForGroup = linkedMapOf(
|
||||
"avatar_heart" to DefaultAvatar(R.drawable.ic_avatar_heart, "A180"),
|
||||
"avatar_house" to DefaultAvatar(R.drawable.ic_avatar_house, "A120"),
|
||||
"avatar_melon" to DefaultAvatar(R.drawable.ic_avatar_melon, "A110"),
|
||||
"avatar_drink" to DefaultAvatar(R.drawable.ic_avatar_drink, "A170"),
|
||||
"avatar_celebration" to DefaultAvatar(R.drawable.ic_avatar_celebration, "A100"),
|
||||
"avatar_balloon" to DefaultAvatar(R.drawable.ic_avatar_balloon, "A220"),
|
||||
"avatar_book" to DefaultAvatar(R.drawable.ic_avatar_book, "A100"),
|
||||
"avatar_briefcase" to DefaultAvatar(R.drawable.ic_avatar_briefcase, "A180"),
|
||||
"avatar_sunset" to DefaultAvatar(R.drawable.ic_avatar_sunset, "A120"),
|
||||
"avatar_surfboard" to DefaultAvatar(R.drawable.ic_avatar_surfboard, "A110"),
|
||||
"avatar_soccerball" to DefaultAvatar(R.drawable.ic_avatar_soccerball, "A130"),
|
||||
"avatar_football" to DefaultAvatar(R.drawable.ic_avatar_football, "A220"),
|
||||
)
|
||||
|
||||
@DrawableRes
|
||||
fun getDrawableResource(key: String): Int? {
|
||||
val defaultAvatar = defaultAvatarsForSelf.getOrDefault(key, defaultAvatarsForGroup[key])
|
||||
|
||||
return defaultAvatar?.vectorDrawableId
|
||||
}
|
||||
|
||||
private fun textPaint(context: Context) = Paint().apply {
|
||||
isAntiAlias = true
|
||||
typeface = AvatarRenderer.getTypeface(context)
|
||||
textSize = 1f
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the text size for a give string using a maximum desired width and a maximum desired font size.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getTextSizeForLength(context: Context, text: String, @Px maxWidth: Float, @Px maxSize: Float): Float {
|
||||
val paint = textPaint(context)
|
||||
return branchSizes(0f, maxWidth / 2, maxWidth, maxSize, paint, text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses binary search to determine optimal font size to within 1% given the input parameters.
|
||||
*/
|
||||
private fun branchSizes(@Px lastFontSize: Float, @Px fontSize: Float, @Px target: Float, @Px maxFontSize: Float, paint: Paint, text: String): Float {
|
||||
paint.textSize = fontSize
|
||||
val textWidth = paint.measureText(text)
|
||||
val delta = abs(lastFontSize - fontSize) / 2f
|
||||
val isWithinThreshold = abs(1f - (textWidth / target)) <= 0.01f
|
||||
|
||||
if (textWidth == 0f) {
|
||||
return maxFontSize
|
||||
}
|
||||
|
||||
if (delta == 0f) {
|
||||
return min(maxFontSize, fontSize)
|
||||
}
|
||||
|
||||
return when {
|
||||
fontSize >= maxFontSize -> {
|
||||
maxFontSize
|
||||
}
|
||||
isWithinThreshold -> {
|
||||
fontSize
|
||||
}
|
||||
textWidth > target -> {
|
||||
branchSizes(fontSize, fontSize - delta, target, maxFontSize, paint, text)
|
||||
}
|
||||
else -> {
|
||||
branchSizes(fontSize, fontSize + delta, target, maxFontSize, paint, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getForegroundColor(avatarColor: AvatarColor): ForegroundColor {
|
||||
return ForegroundColor.values().firstOrNull { it.serialize() == avatarColor.serialize() } ?: ForegroundColor.A210
|
||||
}
|
||||
|
||||
data class DefaultAvatar(
|
||||
@DrawableRes val vectorDrawableId: Int,
|
||||
val colorCode: String
|
||||
)
|
||||
|
||||
data class ColorPair(
|
||||
val backgroundAvatarColor: AvatarColor,
|
||||
val foregroundAvatarColor: ForegroundColor
|
||||
) {
|
||||
@ColorInt val backgroundColor: Int = backgroundAvatarColor.colorInt()
|
||||
@ColorInt val foregroundColor: Int = foregroundAvatarColor.colorInt
|
||||
val code: String = backgroundAvatarColor.serialize()
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Layout
|
||||
import android.text.SpannableString
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import androidx.core.graphics.withTranslation
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider
|
||||
|
||||
class TextAvatarDrawable(
|
||||
private val context: Context,
|
||||
private val avatar: Avatar.Text,
|
||||
inverted: Boolean = false,
|
||||
private val size: Int = AvatarRenderer.DIMENSIONS,
|
||||
private val synchronous: Boolean = false
|
||||
) : Drawable() {
|
||||
|
||||
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
|
||||
init {
|
||||
textPaint.typeface = AvatarRenderer.getTypeface(context)
|
||||
textPaint.color = if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor
|
||||
textPaint.density = context.resources.displayMetrics.density
|
||||
|
||||
setBounds(0, 0, size, size)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
|
||||
val width = bounds.width()
|
||||
val candidates = EmojiProvider.getCandidates(avatar.text)
|
||||
var hasEmoji = false
|
||||
|
||||
textPaint.textSize = textSize
|
||||
|
||||
val newText = if (candidates == null || candidates.size() == 0) {
|
||||
SpannableString(avatar.text)
|
||||
} else {
|
||||
EmojiProvider.emojify(context, candidates, avatar.text, textPaint, synchronous)
|
||||
}
|
||||
|
||||
if (newText == null) return
|
||||
|
||||
val layout = StaticLayout(SpannableString(newText), textPaint, width, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
|
||||
layout.draw(canvas, getStartX(layout), ((bounds.height() / 2) - ((layout.height / 2))).toFloat())
|
||||
}
|
||||
|
||||
private fun getStartX(layout: StaticLayout): Float {
|
||||
val direction = layout.getParagraphDirection(0)
|
||||
val lineWidth = layout.getLineWidth(0)
|
||||
val width = bounds.width()
|
||||
val xPos = (width - lineWidth) / 2
|
||||
return if (direction == Layout.DIR_LEFT_TO_RIGHT) xPos else -xPos
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
|
||||
private fun Layout.draw(canvas: Canvas, x: Float, y: Float) {
|
||||
canvas.withTranslation(x, y) {
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar.photo
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.Navigation
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
||||
|
||||
class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), ImageEditorFragment.Controller {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val args = PhotoEditorFragmentArgs.fromBundle(requireArguments())
|
||||
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
|
||||
val imageEditorFragment = ImageEditorFragment.newInstanceForAvatarEdit(photo.uri)
|
||||
|
||||
childFragmentManager.commit {
|
||||
add(R.id.fragment_container, imageEditorFragment, IMAGE_EDITOR)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEventsNeeded(needed: Boolean) {
|
||||
}
|
||||
|
||||
override fun onRequestFullScreen(fullScreen: Boolean, hideKeyboard: Boolean) {
|
||||
}
|
||||
|
||||
override fun onDoneEditing() {
|
||||
val args = PhotoEditorFragmentArgs.fromBundle(requireArguments())
|
||||
val applicationContext = requireContext().applicationContext
|
||||
val imageEditorFragment: ImageEditorFragment = childFragmentManager.findFragmentByTag(IMAGE_EDITOR) as ImageEditorFragment
|
||||
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val editedImageUri = imageEditorFragment.renderToSingleUseBlob()
|
||||
val size = BlobProvider.getFileSize(editedImageUri) ?: 0
|
||||
val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri)
|
||||
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
|
||||
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
|
||||
val database = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val newPhoto = photo.copy(uri = onDiskUri, size = size)
|
||||
|
||||
database.update(newPhoto)
|
||||
BlobProvider.getInstance().delete(requireContext(), photo.uri)
|
||||
|
||||
ThreadUtil.runOnMain {
|
||||
setFragmentResult(REQUEST_KEY_EDIT, AvatarBundler.bundlePhoto(newPhoto))
|
||||
Navigation.findNavController(requireView()).popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelEditing() {
|
||||
Navigation.findNavController(requireView()).popBackStack()
|
||||
}
|
||||
|
||||
override fun onMainImageLoaded() {
|
||||
}
|
||||
|
||||
override fun onMainImageFailedToLoad() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY_EDIT = "org.thoughtcrime.securesms.avatar.photo.EDIT"
|
||||
|
||||
private const val IMAGE_EDITOR = "image_editor"
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar.picker
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
import org.thoughtcrime.securesms.avatar.photo.PhotoEditorFragment
|
||||
import org.thoughtcrime.securesms.avatar.text.TextAvatarCreationFragment
|
||||
import org.thoughtcrime.securesms.avatar.vector.VectorAvatarCreationFragment
|
||||
import org.thoughtcrime.securesms.components.ButtonStripItemView
|
||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
|
||||
*/
|
||||
class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY_SELECT_AVATAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR"
|
||||
const val SELECT_AVATAR_MEDIA = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_MEDIA"
|
||||
const val SELECT_AVATAR_CLEAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_CLEAR"
|
||||
|
||||
private const val REQUEST_CODE_SELECT_IMAGE = 1
|
||||
}
|
||||
|
||||
private val viewModel: AvatarPickerViewModel by viewModels(factoryProducer = this::createFactory)
|
||||
|
||||
private lateinit var recycler: RecyclerView
|
||||
|
||||
private fun createFactory(): AvatarPickerViewModel.Factory {
|
||||
val args = AvatarPickerFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = ParcelableGroupId.get(args.groupId)
|
||||
|
||||
return AvatarPickerViewModel.Factory(AvatarPickerRepository(requireContext()), groupId, args.isNewGroup, args.groupAvatarMedia)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.avatar_picker_toolbar)
|
||||
val cameraButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_camera)
|
||||
val photoButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_photo)
|
||||
val textButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_text)
|
||||
val saveButton: View = view.findViewById(R.id.avatar_picker_save)
|
||||
val clearButton: View = view.findViewById(R.id.avatar_picker_clear)
|
||||
|
||||
recycler = view.findViewById(R.id.avatar_picker_recycler)
|
||||
recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16)))
|
||||
|
||||
val adapter = MappingAdapter()
|
||||
AvatarPickerItem.register(adapter, this::onAvatarClick, this::onAvatarLongClick)
|
||||
|
||||
recycler.adapter = adapter
|
||||
|
||||
val avatarViewHolder = AvatarPickerItem.ViewHolder(view)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
if (state.currentAvatar != null) {
|
||||
avatarViewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false))
|
||||
}
|
||||
|
||||
clearButton.visible = state.canClear
|
||||
|
||||
val wasEnabled = saveButton.isEnabled
|
||||
saveButton.isEnabled = state.canSave
|
||||
if (wasEnabled != state.canSave) {
|
||||
val alpha = if (state.canSave) 1f else 0.5f
|
||||
saveButton.animate().cancel()
|
||||
saveButton.animate().alpha(alpha)
|
||||
}
|
||||
|
||||
val items = state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) }
|
||||
val selectedPosition = items.indexOfFirst { it.isSelected }
|
||||
|
||||
adapter.submitList(items) {
|
||||
if (selectedPosition > -1)
|
||||
recycler.smoothScrollToPosition(selectedPosition)
|
||||
}
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener { Navigation.findNavController(it).popBackStack() }
|
||||
cameraButton.setOnIconClickedListener { openCameraCapture() }
|
||||
photoButton.setOnIconClickedListener { openGallery() }
|
||||
textButton.setOnIconClickedListener { openTextEditor(null) }
|
||||
saveButton.setOnClickListener { v ->
|
||||
viewModel.save(
|
||||
{
|
||||
setFragmentResult(
|
||||
REQUEST_KEY_SELECT_AVATAR,
|
||||
Bundle().apply {
|
||||
putParcelable(SELECT_AVATAR_MEDIA, it)
|
||||
}
|
||||
)
|
||||
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
|
||||
},
|
||||
{
|
||||
setFragmentResult(
|
||||
REQUEST_KEY_SELECT_AVATAR,
|
||||
Bundle().apply {
|
||||
putBoolean(SELECT_AVATAR_CLEAR, true)
|
||||
}
|
||||
)
|
||||
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
|
||||
}
|
||||
)
|
||||
}
|
||||
clearButton.setOnClickListener { viewModel.clear() }
|
||||
|
||||
setFragmentResultListener(TextAvatarCreationFragment.REQUEST_KEY_TEXT) { _, bundle ->
|
||||
val text = AvatarBundler.extractText(bundle)
|
||||
viewModel.onAvatarEditCompleted(text)
|
||||
}
|
||||
|
||||
setFragmentResultListener(VectorAvatarCreationFragment.REQUEST_KEY_VECTOR) { _, bundle ->
|
||||
val vector = AvatarBundler.extractVector(bundle)
|
||||
viewModel.onAvatarEditCompleted(vector)
|
||||
}
|
||||
|
||||
setFragmentResultListener(PhotoEditorFragment.REQUEST_KEY_EDIT) { _, bundle ->
|
||||
val photo = AvatarBundler.extractPhoto(bundle)
|
||||
viewModel.onAvatarEditCompleted(photo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ViewUtil.hideKeyboard(requireContext(), requireView())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
val media: Media = requireNotNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
|
||||
viewModel.onAvatarPhotoSelectionCompleted(media)
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAvatarClick(avatar: Avatar, isSelected: Boolean) {
|
||||
if (isSelected) {
|
||||
openEditor(avatar)
|
||||
} else {
|
||||
viewModel.onAvatarSelectedFromGrid(avatar)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAvatarLongClick(anchorView: View, avatar: Avatar): Boolean {
|
||||
val menuRes = when (avatar) {
|
||||
is Avatar.Photo -> R.menu.avatar_picker_context
|
||||
is Avatar.Text -> R.menu.avatar_picker_context
|
||||
is Avatar.Vector -> return true
|
||||
is Avatar.Resource -> return true
|
||||
}
|
||||
|
||||
val popup = PopupMenu(context, anchorView, Gravity.TOP)
|
||||
popup.menuInflater.inflate(menuRes, popup.menu)
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_delete -> viewModel.delete(avatar)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
popup.show()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun openEditor(avatar: Avatar) {
|
||||
when (avatar) {
|
||||
is Avatar.Photo -> openPhotoEditor(avatar)
|
||||
is Avatar.Resource -> throw UnsupportedOperationException()
|
||||
is Avatar.Text -> openTextEditor(avatar)
|
||||
is Avatar.Vector -> openVectorEditor(avatar)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPhotoEditor(photo: Avatar.Photo) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
|
||||
}
|
||||
|
||||
private fun openVectorEditor(vector: Avatar.Vector) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
|
||||
}
|
||||
|
||||
private fun openTextEditor(text: Avatar.Text?) {
|
||||
val bundle = if (text != null) AvatarBundler.bundleText(text) else null
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openCameraCapture() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.onAllGranted {
|
||||
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
||||
}
|
||||
.onAnyDenied {
|
||||
Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
.execute()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openGallery() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted {
|
||||
val intent = AvatarSelectionActivity.getIntentForGallery(requireContext())
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
||||
}
|
||||
.onAnyDenied {
|
||||
Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar.picker
|
||||
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.setPadding
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
typealias OnAvatarClickListener = (Avatar, Boolean) -> Unit
|
||||
typealias OnAvatarLongClickListener = (View, Avatar) -> Boolean
|
||||
|
||||
object AvatarPickerItem {
|
||||
|
||||
private val SELECTION_CHANGED = Any()
|
||||
|
||||
fun register(adapter: MappingAdapter, onAvatarClickListener: OnAvatarClickListener, onAvatarLongClickListener: OnAvatarLongClickListener) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onAvatarClickListener, onAvatarLongClickListener) }, R.layout.avatar_picker_item))
|
||||
}
|
||||
|
||||
class Model(val avatar: Avatar, val isSelected: Boolean) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = avatar.isSameAs(newItem.avatar)
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = avatar == newItem.avatar && isSelected == newItem.isSelected
|
||||
|
||||
override fun getChangePayload(newItem: Model): Any? {
|
||||
return if (newItem.avatar == avatar && isSelected != newItem.isSelected) {
|
||||
SELECTION_CHANGED
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(
|
||||
itemView: View,
|
||||
private val onAvatarClickListener: OnAvatarClickListener? = null,
|
||||
private val onAvatarLongClickListener: OnAvatarLongClickListener? = null
|
||||
) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val imageView: ImageView = itemView.findViewById(R.id.avatar_picker_item_image)
|
||||
private val textView: TextView = itemView.findViewById(R.id.avatar_picker_item_text)
|
||||
private val selectedFader: View? = itemView.findViewById(R.id.avatar_picker_item_fader)
|
||||
private val selectedOverlay: View? = itemView.findViewById(R.id.avatar_picker_item_selection_overlay)
|
||||
|
||||
init {
|
||||
textView.typeface = AvatarRenderer.getTypeface(context)
|
||||
textView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
updateFontSize(textView.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFontSize(text: String) {
|
||||
val textSize = Avatars.getTextSizeForLength(context, text, textView.measuredWidth * 0.8f, textView.measuredHeight * 0.45f)
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
|
||||
if (textView !is EditText) {
|
||||
textView.text = text
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
val alpha = if (model.isSelected) 1f else 0f
|
||||
val scale = if (model.isSelected) 0.9f else 1f
|
||||
|
||||
imageView.animate().cancel()
|
||||
textView.animate().cancel()
|
||||
selectedOverlay?.animate()?.cancel()
|
||||
selectedFader?.animate()?.cancel()
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
onAvatarLongClickListener?.invoke(itemView, model.avatar) ?: false
|
||||
}
|
||||
|
||||
itemView.setOnClickListener { onAvatarClickListener?.invoke(model.avatar, model.isSelected) }
|
||||
|
||||
if (payload.isNotEmpty() && payload.contains(SELECTION_CHANGED)) {
|
||||
imageView.animate().scaleX(scale).scaleY(scale)
|
||||
textView.animate().scaleX(scale).scaleY(scale)
|
||||
selectedOverlay?.animate()?.alpha(alpha)
|
||||
selectedFader?.animate()?.alpha(alpha)
|
||||
return
|
||||
}
|
||||
|
||||
imageView.scaleX = scale
|
||||
imageView.scaleY = scale
|
||||
textView.scaleX = scale
|
||||
textView.scaleY = scale
|
||||
selectedFader?.alpha = alpha
|
||||
selectedOverlay?.alpha = alpha
|
||||
|
||||
imageView.clearColorFilter()
|
||||
imageView.setPadding(0)
|
||||
|
||||
when (model.avatar) {
|
||||
is Avatar.Text -> {
|
||||
textView.visible = true
|
||||
|
||||
updateFontSize(model.avatar.text)
|
||||
if (textView.text.toString() != model.avatar.text) {
|
||||
textView.text = model.avatar.text
|
||||
}
|
||||
|
||||
imageView.setImageDrawable(null)
|
||||
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
|
||||
textView.setTextColor(model.avatar.color.foregroundColor)
|
||||
}
|
||||
is Avatar.Vector -> {
|
||||
textView.visible = false
|
||||
|
||||
val drawableId = Avatars.getDrawableResource(model.avatar.key)
|
||||
if (drawableId == null) {
|
||||
imageView.setImageDrawable(null)
|
||||
} else {
|
||||
imageView.setImageDrawable(AppCompatResources.getDrawable(context, drawableId))
|
||||
}
|
||||
|
||||
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
|
||||
}
|
||||
is Avatar.Photo -> {
|
||||
textView.visible = false
|
||||
GlideApp.with(imageView).load(DecryptableStreamUriLoader.DecryptableUri(model.avatar.uri)).into(imageView)
|
||||
}
|
||||
is Avatar.Resource -> {
|
||||
imageView.setPadding((imageView.width * 0.2).toInt())
|
||||
textView.visible = false
|
||||
GlideApp.with(imageView).clear(imageView)
|
||||
imageView.setImageResource(model.avatar.resourceId)
|
||||
imageView.colorFilter = SimpleColorFilter(model.avatar.color.foregroundColor)
|
||||
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar.picker
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.NameUtil
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = Log.tag(AvatarPickerRepository::class.java)
|
||||
|
||||
class AvatarPickerRepository(context: Context) {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
|
||||
fun getAvatarForSelf(): Single<Avatar> = Single.fromCallable {
|
||||
val details: StreamDetails? = AvatarHelper.getSelfProfileAvatarStream(applicationContext)
|
||||
if (details != null) {
|
||||
try {
|
||||
val bytes = StreamUtil.readFully(details.stream)
|
||||
Avatar.Photo(
|
||||
BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(),
|
||||
details.length,
|
||||
Avatar.DatabaseId.DoNotPersist
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to read avatar!")
|
||||
getDefaultAvatarForSelf()
|
||||
}
|
||||
} else {
|
||||
getDefaultAvatarForSelf()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAvatarForGroup(groupId: GroupId): Single<Avatar> = Single.fromCallable {
|
||||
val recipient = Recipient.externalGroupExact(applicationContext, groupId)
|
||||
|
||||
if (AvatarHelper.hasAvatar(applicationContext, recipient.id)) {
|
||||
try {
|
||||
val bytes = AvatarHelper.getAvatarBytes(applicationContext, recipient.id)
|
||||
Avatar.Photo(
|
||||
BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(),
|
||||
AvatarHelper.getAvatarLength(applicationContext, recipient.id),
|
||||
Avatar.DatabaseId.DoNotPersist
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to read group avatar!")
|
||||
getDefaultAvatarForGroup(recipient.avatarColor)
|
||||
}
|
||||
} else {
|
||||
getDefaultAvatarForGroup(recipient.avatarColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPersistedAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
||||
DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForSelf()
|
||||
}
|
||||
|
||||
fun getPersistedAvatarsForGroup(groupId: GroupId): Single<List<Avatar>> = Single.fromCallable {
|
||||
DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForGroup(groupId)
|
||||
}
|
||||
|
||||
fun getDefaultAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
||||
Avatars.defaultAvatarsForSelf.entries.mapIndexed { index, entry ->
|
||||
Avatar.Vector(entry.key, color = Avatars.colors[index % Avatars.colors.size], Avatar.DatabaseId.NotSet)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultAvatarsForGroup(): Single<List<Avatar>> = Single.fromCallable {
|
||||
Avatars.defaultAvatarsForGroup.entries.mapIndexed { index, entry ->
|
||||
Avatar.Vector(entry.key, color = Avatars.colors[index % Avatars.colors.size], Avatar.DatabaseId.NotSet)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeMediaToMultiSessionStorage(media: Media, onMediaWrittenToMultiSessionStorage: (Uri) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
onMediaWrittenToMultiSessionStorage(AvatarPickerStorage.save(applicationContext, media))
|
||||
}
|
||||
}
|
||||
|
||||
fun persistAvatarForSelf(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val savedAvatar = avatarDatabase.saveAvatarForSelf(avatar)
|
||||
avatarDatabase.markUsage(savedAvatar)
|
||||
onPersisted(savedAvatar)
|
||||
}
|
||||
}
|
||||
|
||||
fun persistAvatarForGroup(avatar: Avatar, groupId: GroupId, onPersisted: (Avatar) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val savedAvatar = avatarDatabase.saveAvatarForGroup(avatar, groupId)
|
||||
avatarDatabase.markUsage(savedAvatar)
|
||||
onPersisted(savedAvatar)
|
||||
}
|
||||
}
|
||||
|
||||
fun persistAndCreateMediaForSelf(avatar: Avatar, onSaved: (Media) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
if (avatar.databaseId !is Avatar.DatabaseId.DoNotPersist) {
|
||||
persistAvatarForSelf(avatar) {
|
||||
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
|
||||
}
|
||||
} else {
|
||||
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun persistAndCreateMediaForGroup(avatar: Avatar, groupId: GroupId, onSaved: (Media) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
if (avatar.databaseId !is Avatar.DatabaseId.DoNotPersist) {
|
||||
persistAvatarForGroup(avatar, groupId) {
|
||||
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
|
||||
}
|
||||
} else {
|
||||
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createMediaForNewGroup(avatar: Avatar, onSaved: (Media) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleRenderFailure(throwable: Throwable?) {
|
||||
Log.w(TAG, "Failed to render avatar.", throwable)
|
||||
ThreadUtil.postToMain {
|
||||
Toast.makeText(applicationContext, R.string.AvatarPickerRepository__failed_to_save_avatar, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultAvatarForSelf(): Avatar {
|
||||
val initials = NameUtil.getAbbreviation(Recipient.self().getDisplayName(applicationContext))
|
||||
|
||||
return if (initials.isNullOrBlank()) {
|
||||
Avatar.getDefaultForSelf()
|
||||
} else {
|
||||
Avatar.Text(initials, requireNotNull(Avatars.colorMap[Recipient.self().avatarColor.serialize()]), Avatar.DatabaseId.DoNotPersist)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultAvatarForGroup(groupId: GroupId): Avatar {
|
||||
val recipient = Recipient.externalGroupExact(applicationContext, groupId)
|
||||
|
||||
return getDefaultAvatarForGroup(recipient.avatarColor)
|
||||
}
|
||||
|
||||
fun getDefaultAvatarForGroup(color: AvatarColor?): Avatar {
|
||||
val colorPair = Avatars.colorMap[color?.serialize()]
|
||||
val defaultColor = Avatar.getDefaultForGroup()
|
||||
|
||||
return if (colorPair != null) {
|
||||
defaultColor.copy(color = colorPair)
|
||||
} else {
|
||||
defaultColor
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(avatar: Avatar, onDelete: () -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
if (avatar.databaseId is Avatar.DatabaseId.Saved) {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
avatarDatabase.deleteAvatar(avatar)
|
||||
}
|
||||
onDelete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar.picker
|
||||
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
|
||||
data class AvatarPickerState(
|
||||
val currentAvatar: Avatar? = null,
|
||||
val selectableAvatars: List<Avatar> = listOf(),
|
||||
val canSave: Boolean = false,
|
||||
val canClear: Boolean = false,
|
||||
val isCleared: Boolean = false
|
||||
)
|
||||
@@ -1,198 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar.picker
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepository) : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val store = Store(AvatarPickerState())
|
||||
|
||||
val state: LiveData<AvatarPickerState> = store.stateLiveData
|
||||
|
||||
protected abstract fun getAvatar(): Single<Avatar>
|
||||
protected abstract fun getDefaultAvatarFromRepository(): Avatar
|
||||
protected abstract fun getPersistedAvatars(): Single<List<Avatar>>
|
||||
protected abstract fun getDefaultAvatars(): Single<List<Avatar>>
|
||||
protected abstract fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit)
|
||||
protected abstract fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit)
|
||||
|
||||
fun delete(avatar: Avatar) {
|
||||
repository.delete(avatar) {
|
||||
refreshAvatar()
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
store.update {
|
||||
val avatar = getDefaultAvatarFromRepository()
|
||||
it.copy(currentAvatar = avatar, canSave = true, canClear = false, isCleared = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun save(onSaved: (Media) -> Unit, onCleared: () -> Unit) {
|
||||
if (store.state.isCleared) {
|
||||
onCleared()
|
||||
} else {
|
||||
val avatar = store.state.currentAvatar ?: throw AssertionError()
|
||||
persistAndCreateMedia(avatar, onSaved)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAvatarSelectedFromGrid(avatar: Avatar) {
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true, isCleared = false) }
|
||||
}
|
||||
|
||||
fun onAvatarEditCompleted(avatar: Avatar) {
|
||||
persistAvatar(avatar) { saved ->
|
||||
store.update { it.copy(currentAvatar = saved, canSave = isSaveable(saved), canClear = true, isCleared = false) }
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
}
|
||||
|
||||
fun onAvatarPhotoSelectionCompleted(media: Media) {
|
||||
repository.writeMediaToMultiSessionStorage(media) { multiSessionUri ->
|
||||
persistAvatar(Avatar.Photo(multiSessionUri, media.size, Avatar.DatabaseId.NotSet)) { avatar ->
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true, isCleared = false) }
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun refreshAvatar() {
|
||||
disposables.add(
|
||||
getAvatar().subscribeOn(Schedulers.io()).subscribe { avatar ->
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = avatar is Avatar.Photo && !isSaveable(avatar), isCleared = false) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
protected fun refreshSelectableAvatars() {
|
||||
disposables.add(
|
||||
Single.zip(getPersistedAvatars(), getDefaultAvatars()) { custom, def ->
|
||||
val customKeys = custom.filterIsInstance(Avatar.Vector::class.java).map { it.key }
|
||||
custom + def.filterNot {
|
||||
it is Avatar.Vector && customKeys.contains(it.key)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io()).subscribe { avatars ->
|
||||
store.update { it.copy(selectableAvatars = avatars) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun isSaveable(avatar: Avatar) = avatar.databaseId != Avatar.DatabaseId.DoNotPersist
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
private class SelfAvatarPickerViewModel(private val repository: AvatarPickerRepository) : AvatarPickerViewModel(repository) {
|
||||
|
||||
init {
|
||||
refreshAvatar()
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
|
||||
override fun getAvatar(): Single<Avatar> = repository.getAvatarForSelf()
|
||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForSelf()
|
||||
override fun getPersistedAvatars(): Single<List<Avatar>> = repository.getPersistedAvatarsForSelf()
|
||||
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForSelf()
|
||||
|
||||
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
|
||||
repository.persistAvatarForSelf(avatar, onPersisted)
|
||||
}
|
||||
|
||||
override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) {
|
||||
repository.persistAndCreateMediaForSelf(avatar, onSaved)
|
||||
}
|
||||
}
|
||||
|
||||
private class GroupAvatarPickerViewModel(
|
||||
private val groupId: GroupId,
|
||||
private val repository: AvatarPickerRepository,
|
||||
groupAvatarMedia: Media?
|
||||
) : AvatarPickerViewModel(repository) {
|
||||
|
||||
private val initialAvatar: Avatar? = groupAvatarMedia?.let { Avatar.Photo(it.uri, it.size, Avatar.DatabaseId.DoNotPersist) }
|
||||
|
||||
init {
|
||||
refreshAvatar()
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
|
||||
override fun getAvatar(): Single<Avatar> {
|
||||
return if (initialAvatar != null) {
|
||||
Single.just(initialAvatar)
|
||||
} else {
|
||||
repository.getAvatarForGroup(groupId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup(groupId)
|
||||
override fun getPersistedAvatars(): Single<List<Avatar>> = repository.getPersistedAvatarsForGroup(groupId)
|
||||
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
|
||||
|
||||
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
|
||||
repository.persistAvatarForGroup(avatar, groupId, onPersisted)
|
||||
}
|
||||
|
||||
override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) {
|
||||
repository.persistAndCreateMediaForGroup(avatar, groupId, onSaved)
|
||||
}
|
||||
}
|
||||
|
||||
private class NewGroupAvatarPickerViewModel(
|
||||
private val repository: AvatarPickerRepository,
|
||||
initialMedia: Media?
|
||||
) : AvatarPickerViewModel(repository) {
|
||||
|
||||
private val initialAvatar: Avatar? = initialMedia?.let { Avatar.Photo(it.uri, it.size, Avatar.DatabaseId.DoNotPersist) }
|
||||
|
||||
init {
|
||||
refreshAvatar()
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
|
||||
override fun getAvatar(): Single<Avatar> {
|
||||
return if (initialAvatar != null) {
|
||||
Single.just(initialAvatar)
|
||||
} else {
|
||||
Single.fromCallable { getDefaultAvatarFromRepository() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup(null)
|
||||
override fun getPersistedAvatars(): Single<List<Avatar>> = Single.just(listOf())
|
||||
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
|
||||
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) = onPersisted(avatar)
|
||||
override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) = repository.createMediaForNewGroup(avatar, onSaved)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val repository: AvatarPickerRepository,
|
||||
private val groupId: GroupId?,
|
||||
private val isNewGroup: Boolean,
|
||||
private val groupAvatarMedia: Media?
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
val viewModel = if (groupId == null && !isNewGroup) {
|
||||
SelfAvatarPickerViewModel(repository)
|
||||
} else if (groupId == null) {
|
||||
NewGroupAvatarPickerViewModel(repository, groupAvatarMedia)
|
||||
} else {
|
||||
GroupAvatarPickerViewModel(groupId, repository, groupAvatarMedia)
|
||||
}
|
||||
|
||||
return requireNotNull(modelClass.cast(viewModel))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package org.thoughtcrime.securesms.avatar.text
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import org.signal.core.util.EditTextUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
import org.thoughtcrime.securesms.avatar.AvatarColorItem
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerItem
|
||||
import org.thoughtcrime.securesms.components.BoldSelectionTabItem
|
||||
import org.thoughtcrime.securesms.components.ControllableTabLayout
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
* Fragment to create an avatar based off of a Vector or Text (via a pager)
|
||||
*/
|
||||
class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragment) {
|
||||
|
||||
private val viewModel: TextAvatarCreationViewModel by viewModels(factoryProducer = this::createFactory)
|
||||
|
||||
private lateinit var textInput: EditText
|
||||
private lateinit var recycler: RecyclerView
|
||||
private lateinit var content: ConstraintLayout
|
||||
|
||||
private val withRecyclerSet = ConstraintSet()
|
||||
private val withoutRecyclerSet = ConstraintSet()
|
||||
|
||||
private var hasBoundFromViewModel: Boolean = false
|
||||
|
||||
private fun createFactory(): TextAvatarCreationViewModel.Factory {
|
||||
val args = TextAvatarCreationFragmentArgs.fromBundle(requireArguments())
|
||||
val textBundle = args.textAvatar
|
||||
val text = if (textBundle != null) {
|
||||
AvatarBundler.extractText(textBundle)
|
||||
} else {
|
||||
Avatar.Text("", Avatars.colors.random(), Avatar.DatabaseId.NotSet)
|
||||
}
|
||||
|
||||
return TextAvatarCreationViewModel.Factory(text)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.text_avatar_creation_toolbar)
|
||||
val tabLayout: ControllableTabLayout = view.findViewById(R.id.text_avatar_creation_tabs)
|
||||
val doneButton: View = view.findViewById(R.id.text_avatar_creation_done)
|
||||
val keyboardAwareLayout: KeyboardAwareLinearLayout = view.findViewById(R.id.keyboard_aware_layout)
|
||||
|
||||
withRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content)
|
||||
withoutRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content_hidden_recycler)
|
||||
|
||||
content = view.findViewById(R.id.content)
|
||||
recycler = view.findViewById(R.id.text_avatar_creation_recycler)
|
||||
textInput = view.findViewById(R.id.avatar_picker_item_text)
|
||||
|
||||
toolbar.setNavigationOnClickListener { Navigation.findNavController(it).popBackStack() }
|
||||
BoldSelectionTabItem.registerListeners(tabLayout)
|
||||
|
||||
val onTabSelectedListener = OnTabSelectedListener()
|
||||
tabLayout.addOnTabSelectedListener(onTabSelectedListener)
|
||||
onTabSelectedListener.onTabSelected(requireNotNull(tabLayout.getTabAt(tabLayout.selectedTabPosition)))
|
||||
|
||||
val adapter = MappingAdapter()
|
||||
recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16)))
|
||||
AvatarColorItem.registerViewHolder(adapter) {
|
||||
viewModel.setColor(it)
|
||||
}
|
||||
recycler.adapter = adapter
|
||||
|
||||
val viewHolder = AvatarPickerItem.ViewHolder(view)
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
EditTextUtil.setCursorColor(textInput, state.currentAvatar.color.foregroundColor)
|
||||
viewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false))
|
||||
|
||||
adapter.submitList(state.colors().map { AvatarColorItem.Model(it) })
|
||||
hasBoundFromViewModel = true
|
||||
}
|
||||
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(textInput, 3)
|
||||
textInput.doAfterTextChanged {
|
||||
if (it != null && hasBoundFromViewModel) {
|
||||
viewModel.setText(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
doneButton.setOnClickListener { v ->
|
||||
setFragmentResult(REQUEST_KEY_TEXT, AvatarBundler.bundleText(viewModel.getCurrentAvatar()))
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
}
|
||||
|
||||
textInput.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
tabLayout.getTabAt(1)?.select()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
keyboardAwareLayout.addOnKeyboardHiddenListener {
|
||||
if (tabLayout.selectedTabPosition == 1) {
|
||||
val transition = AutoTransition().setStartDelay(250L)
|
||||
TransitionManager.endTransitions(content)
|
||||
withRecyclerSet.applyTo(content)
|
||||
TransitionManager.beginDelayedTransition(content, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OnTabSelectedListener : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
when (tab.position) {
|
||||
0 -> {
|
||||
textInput.isEnabled = true
|
||||
ViewUtil.focusAndShowKeyboard(textInput)
|
||||
|
||||
withoutRecyclerSet.applyTo(content)
|
||||
textInput.setSelection(textInput.length())
|
||||
}
|
||||
1 -> {
|
||||
textInput.isEnabled = false
|
||||
ViewUtil.hideKeyboard(requireContext(), textInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY_TEXT = "org.thoughtcrime.securesms.avatar.text.TEXT"
|
||||
}
|
||||
}
|
||||