diff --git a/.drone.jsonnet b/.drone.jsonnet
new file mode 100644
index 0000000000..dc81115ce9
--- /dev/null
+++ b/.drone.jsonnet
@@ -0,0 +1,88 @@
+local docker_base = 'registry.oxen.rocks/lokinet-ci-';
+
+// Log a bunch of version information to make it easier for debugging
+local version_info = {
+ name: 'Version Information',
+ image: docker_base + 'android',
+ commands: [
+ 'cmake --version',
+ 'apt --installed list'
+ ]
+};
+
+
+// Intentionally doing a depth of 2 as libSession-util has it's own submodules (and libLokinet likely will as well)
+local clone_submodules = {
+ name: 'Clone Submodules',
+ image: 'drone/git',
+ commands: ['git fetch --tags', 'git submodule update --init --recursive --depth=2 --jobs=4']
+};
+
+// cmake options for static deps mirror
+local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https://oxen.rocks/deps ' else '');
+
+[
+ // Unit tests (PRs only)
+ {
+ kind: 'pipeline',
+ type: 'docker',
+ name: 'Unit Tests',
+ platform: { arch: 'amd64' },
+ trigger: { event: { exclude: [ 'push' ] } },
+ steps: [
+ version_info,
+ clone_submodules,
+ {
+ name: 'Run Unit Tests',
+ image: docker_base + 'android',
+ pull: 'always',
+ environment: { ANDROID_HOME: '/usr/lib/android-sdk' },
+ commands: [
+ 'apt-get install -y ninja-build',
+ './gradlew testPlayDebugUnitTestCoverageReport'
+ ],
+ }
+ ],
+ },
+ // Validate build artifact was created by the direct branch push (PRs only)
+ {
+ kind: 'pipeline',
+ type: 'docker',
+ name: 'Check Build Artifact Existence',
+ platform: { arch: 'amd64' },
+ trigger: { event: { exclude: [ 'push' ] } },
+ steps: [
+ {
+ name: 'Poll for build artifact existence',
+ image: docker_base + 'android',
+ pull: 'always',
+ commands: [
+ './scripts/drone-upload-exists.sh'
+ ]
+ }
+ ]
+ },
+ // Debug APK build (non-PRs only)
+ {
+ kind: 'pipeline',
+ type: 'docker',
+ name: 'Debug APK Build',
+ platform: { arch: 'amd64' },
+ trigger: { event: { exclude: [ 'pull_request' ] } },
+ steps: [
+ version_info,
+ clone_submodules,
+ {
+ name: 'Build and upload',
+ image: docker_base + 'android',
+ pull: 'always',
+ environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, ANDROID_HOME: '/usr/lib/android-sdk' },
+ commands: [
+ 'apt-get install -y ninja-build',
+ './gradlew assemblePlayDebug',
+ './scripts/drone-static-upload.sh'
+ ],
+ }
+ ],
+ }
+]
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index 7c17252c2d..0000000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-- [ ] I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
-- [ ] I have searched open and closed issues for duplicates
-- [ ] I am submitting a bug report for existing functionality that does not work as intended
-- [ ] This isn't a feature request or a discussion topic
-
-----------------------------------------
-
-### Bug description
-Describe here the issue that you are experiencing.
-
-### Steps to reproduce
-- using hyphens as bullet points
-- list the steps
-- that reproduce the bug
-
-**Actual result:**
-
-Describe here what happens after you run the steps above (i.e. the buggy behaviour)
-
-**Expected result:**
-
-Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour)
-
-### Screenshots
-
-
-### Device info
-
-
-**Device:** Manufacturer Model XVI
-
-**Android version:** 0.0.0
-
-**Session version:** 0.0.0
-
-### Link to debug log
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 74bbafd0f6..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: ''
-labels: bug
-assignees: ''
-
----
-
-**Code of conduct**
-
-- [ ] I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
-
-**Describe the bug**
-
-A clear and concise description of what the bug is.
-
-**To reproduce**
-
-Steps to reproduce the behavior:
-
-**Screenshots or logs**
-
-If applicable, add screenshots or logs to help explain your problem.
-
-**Smartphone (please complete the following information):**
-
- - Device: [e.g. Samsung Galaxy S8]
- - OS: [e.g. Android Pie]
- - Version of Session or latest commit hash
-
-**Additional context**
-
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000..883f792482
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,74 @@
+name: 🐞 Bug Report
+description: Create a report to help us improve
+title: "[BUG]
"
+labels: [bug]
+body:
+- type: checkboxes
+ attributes:
+ label: Code of conduct
+ description: I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
+ options:
+ - label: I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md)
+ required: true
+
+- type: checkboxes
+ attributes:
+ label: Self-training on how to write a bug report
+ description: High quality bug reports can help the team save time and improve the chance of getting your issue fixed. Please read [how to write a bug report](https://www.browserstack.com/guide/how-to-write-a-bug-report) before submitting your issue.
+ options:
+ - label: I have learned [how to write a bug report](https://www.browserstack.com/guide/how-to-write-a-bug-report)
+ required: true
+
+- type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue already exists for the bug you encountered.
+ options:
+ - label: I have searched the existing issues
+ required: true
+- type: textarea
+ attributes:
+ label: Current Behavior
+ description: A concise description of what you're experiencing.
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Expected Behavior
+ description: A concise description of what you expected to happen.
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Steps To Reproduce
+ description: Steps to reproduce the behavior.
+ placeholder: |
+ 1. In this environment...
+ 2. With this config...
+ 3. Run '...'
+ 4. See error...
+ validations:
+ required: false
+- type: input
+ attributes:
+ label: Android Version
+ description: What version of Android are you running?
+ placeholder: ex. Android 11
+ validations:
+ required: false
+- type: input
+ attributes:
+ label: Session Version
+ description: What version of Session are you running? (This can be found at the bottom of the app settings)
+ placeholder: ex. 1.17.0 (3425)
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Anything else?
+ description: |
+ Add any other context about the problem here.
+
+ Tip: You can attach screenshots or log files to help explain your problem by clicking this area to highlight it and then dragging files in.
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000000..3c9712e52a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,26 @@
+name: 🚀 Feature request
+description: Suggest an idea for Session
+title: '[Feature] '
+labels: [feature-request]
+body:
+- type: checkboxes
+ attributes:
+ label: Is there an existing request for feature?
+ description: Please search to see if an issue already exists for the feature you are requesting.
+ options:
+ - label: I have searched the existing issues
+ required: true
+- type: textarea
+ attributes:
+ label: What feature would you like?
+ description: |
+ A clear and concise description of the feature you would like added to Session
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Anything else?
+ description: |
+ Add any other context or screenshots about the feature request here
+ validations:
+ required: false
diff --git a/.gitignore b/.gitignore
index 01ec4c41ce..be928b3933 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,8 @@ signing.properties
ffpr
*.sh
pkcs11.password
-play
+app/play
+app/huawei
+
+!/scripts/drone-static-upload.sh
+!/scripts/drone-upload-exists.sh
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..b650b98b11
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "libsession-util/libsession-util"]
+ path = libsession-util/libsession-util
+ url = https://github.com/oxen-io/libsession-util.git
diff --git a/.run/Run Tests.run.xml b/.run/Run Tests.run.xml
new file mode 100644
index 0000000000..42b2e07744
--- /dev/null
+++ b/.run/Run Tests.run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+ false
+
+
+
\ No newline at end of file
diff --git a/BUILDING.md b/BUILDING.md
index e78207d964..48b4412ddd 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -32,6 +32,13 @@ Setting up a development environment and building from Android Studio
4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes".
5. Default config options should be good enough.
6. Project initialization and building should proceed.
+7. Clone submodules with `git submodule update --init --recursive`
+
+If you would like to build the Huawei Flavor with Huawei HMS push notifications you will need to pass 'huawei' as a command line arg to include the required dependencies.
+
+e.g. `./gradlew assembleHuaweiDebug -Phuawei`
+
+If you are building in Android Studio then add `-Phuawei` to `Preferences > Build, Execution, Deployment > Gradle-Android Compiler > Command-line Options`
Contributing code
-----------------
diff --git a/README.md b/README.md
index 341fd42841..17eaebf5fe 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,16 @@
-# Session Android
+# Session Android
[Download on the Google Play Store](https://getsession.org/android)
Add the [F-Droid repo](https://fdroid.getsession.org/)
-[Download the APK from here](https://github.com/loki-project/session-android/releases/latest)
+[Download the APK from here](https://github.com/oxen-io/session-android/releases/latest)
## Summary
Session integrates directly with [Oxen Service Nodes](https://docs.oxen.io/about-the-oxen-blockchain/oxen-service-nodes), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
-
+
## Want to contribute? Found a bug or have a feature request?
diff --git a/app/build.gradle b/app/build.gradle
index 49ecd7a8f5..eb2c16e953 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,24 +1,29 @@
+
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.2.2'
+ classpath "com.android.tools.build:gradle:$gradlePluginVersion"
classpath files('libs/gradle-witness.jar')
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
- classpath "com.google.gms:google-services:4.3.10"
+ classpath "com.google.gms:google-services:$googleServicesVersion"
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
}
}
+plugins {
+ id 'kotlin-kapt'
+ id 'com.google.dagger.hilt.android'
+}
+
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'witness'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
-apply plugin: 'com.google.gms.google-services'
apply plugin: 'kotlinx-serialization'
apply plugin: 'dagger.hilt.android.plugin'
@@ -26,32 +31,244 @@ configurations.all {
exclude module: "commons-logging"
}
+def canonicalVersionCode = 373
+def canonicalVersionName = "1.18.4"
+
+def postFixSize = 10
+def abiPostFix = ['armeabi-v7a' : 1,
+ 'arm64-v8a' : 2,
+ 'x86' : 3,
+ 'x86_64' : 4,
+ 'universal' : 5]
+
+// Function to get the current git commit hash so we can embed it along w/ the build version.
+// Note: This is visible in the SettingsActivity, right at the bottom (R.id.versionTextView).
+def getGitHash = { ->
+ def stdout = new ByteArrayOutputStream()
+ exec {
+ commandLine "git", "rev-parse", "--short", "HEAD"
+ standardOutput = stdout
+ }
+ return stdout.toString().trim()
+}
+
+android {
+ compileSdkVersion androidCompileSdkVersion
+ namespace 'network.loki.messenger'
+ useLibrary 'org.apache.http.legacy'
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ packagingOptions {
+ exclude 'LICENSE.txt'
+ exclude 'LICENSE'
+ exclude 'NOTICE'
+ exclude 'asm-license.txt'
+ exclude 'META-INF/LICENSE'
+ exclude 'META-INF/NOTICE'
+ exclude 'META-INF/proguard/androidx-annotations.pro'
+ }
+
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
+ universalApk true
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.4.7'
+ }
+
+ defaultConfig {
+ versionCode canonicalVersionCode * postFixSize
+ versionName canonicalVersionName
+
+ minSdkVersion androidMinimumSdkVersion
+ targetSdkVersion androidTargetSdkVersion
+
+ multiDexEnabled = true
+
+ vectorDrawables.useSupportLibrary = true
+ project.ext.set("archivesBaseName", "session")
+
+ buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
+ buildConfigField "String", "GIT_HASH", "\"$getGitHash\""
+ buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
+ buildConfigField "int", "CONTENT_PROXY_PORT", "443"
+ buildConfigField "String", "USER_AGENT", "\"OWA\""
+ buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
+ buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
+
+ resConfigs autoResConfig()
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ // The following argument makes the Android Test Orchestrator run its
+ // "pm clear" command after each test invocation. This command ensures
+ // that the app's state is completely cleared between tests.
+ testInstrumentationRunnerArguments clearPackageData: 'true'
+ testOptions {
+ execution 'ANDROIDX_TEST_ORCHESTRATOR'
+ }
+ }
+
+ sourceSets {
+ String sharedTestDir = 'src/sharedTest/java'
+ test.java.srcDirs += sharedTestDir
+ androidTest.java.srcDirs += sharedTestDir
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ debug {
+ isDefault true
+ minifyEnabled false
+ enableUnitTestCoverage true
+ }
+ }
+
+ flavorDimensions "distribution"
+ productFlavors {
+ play {
+ isDefault true
+ dimension "distribution"
+ apply plugin: 'com.google.gms.google-services'
+ ext.websiteUpdateUrl = "null"
+ buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
+ buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
+ buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
+ buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"'
+ }
+
+ huawei {
+ dimension "distribution"
+ ext.websiteUpdateUrl = "null"
+ buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
+ buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.HUAWEI"
+ buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
+ buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"_HUAWEI\"'
+ }
+
+ website {
+ dimension "distribution"
+ ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
+ buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
+ buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
+ buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
+ buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"'
+ }
+ }
+
+ applicationVariants.all { variant ->
+ variant.outputs.each { output ->
+ def abiName = output.getFilter("ABI") ?: 'universal'
+ def postFix = abiPostFix.get(abiName, 0)
+
+ if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
+ output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
+ output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
+ }
+ }
+
+ lintOptions {
+ abortOnError true
+ baseline file("lint-baseline.xml")
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+ buildFeatures {
+ dataBinding true
+ viewBinding true
+ }
+
+ def huaweiEnabled = project.properties['huawei'] != null
+
+ applicationVariants.configureEach { variant ->
+ if (variant.flavorName == 'huawei') {
+ variant.getPreBuildProvider().configure { task ->
+ task.doFirst {
+ if (!huaweiEnabled) {
+ def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See BUILDING.md'
+ logger.error(message)
+ throw new GradleException(message)
+ }
+ }
+ }
+ }
+ }
+
+ task testPlayDebugUnitTestCoverageReport(type: JacocoReport, dependsOn: "testPlayDebugUnitTest") {
+ reports {
+ xml.enabled = true
+ }
+
+ // Add files that should not be listed in the report (e.g. generated Files from dagger)
+ def fileFilter = []
+ def mainSrc = "$projectDir/src/main/java"
+ def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/playDebug", excludes: fileFilter)
+
+ // Compiled Kotlin class files are written into build-variant-specific subdirectories of 'build/tmp/kotlin-classes'.
+ classDirectories.from = files([kotlinDebugTree])
+
+ // To produce an accurate report, the bytecode is mapped back to the original source code.
+ sourceDirectories.from = files([mainSrc])
+
+ // Execution data generated when running the tests against classes instrumented by the JaCoCo agent.
+ // This is enabled with 'enableUnitTestCoverage' in the 'debug' build type.
+ executionData.from = "${project.buildDir}/outputs/unit_test_code_coverage/playDebugUnitTest/testPlayDebugUnitTest.exec"
+ }
+}
+
dependencies {
- implementation 'androidx.appcompat:appcompat:1.3.1'
- implementation 'androidx.recyclerview:recyclerview:1.1.0'
- implementation 'com.google.android.material:material:1.2.1'
+
+ implementation("com.google.dagger:hilt-android:2.46.1")
+ kapt("com.google.dagger:hilt-android-compiler:2.44")
+
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
+ implementation "com.google.android.material:material:$materialVersion"
implementation 'com.google.android:flexbox:2.0.1'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
- implementation 'androidx.preference:preference-ktx:1.1.1'
+ implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
- implementation 'androidx.exifinterface:exifinterface:1.3.3'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ implementation 'androidx.exifinterface:exifinterface:1.3.4'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
- implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
- implementation 'androidx.activity:activity-ktx:1.2.2'
- implementation 'androidx.fragment:fragment-ktx:1.3.2'
- implementation "androidx.core:core-ktx:1.3.2"
- implementation "androidx.work:work-runtime-ktx:2.4.0"
- implementation ("com.google.firebase:firebase-messaging:18.0.0") {
+ implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
+ implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
+ implementation 'androidx.activity:activity-ktx:1.5.1'
+ implementation 'androidx.fragment:fragment-ktx:1.5.3'
+ implementation "androidx.core:core-ktx:$coreVersion"
+ implementation "androidx.work:work-runtime-ktx:2.7.1"
+ playImplementation ("com.google.firebase:firebase-messaging:18.0.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'
}
+ if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0'
@@ -91,20 +308,17 @@ dependencies {
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
- implementation 'com.takisoft.fix:colorpicker:1.0.1'
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 'androidx.sqlite:sqlite-ktx:2.3.1'
+ implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'
implementation project(":libsignal")
implementation project(":libsession")
+ implementation project(":libsession-util")
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
implementation project(":liblazysodium")
- implementation "net.java.dev.jna:jna:5.8.0@aar"
+ implementation "net.java.dev.jna:jna:5.12.1@aar"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
@@ -113,183 +327,59 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
- implementation "com.github.lelloman:android-identicons:v11"
- implementation "com.prof.rssparser:rssparser:2.0.4"
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
implementation "com.github.ybq:Android-SpinKit:1.4.0"
implementation "com.opencsv:opencsv:4.6"
- testImplementation 'junit:junit:4.12'
+ testImplementation "junit:junit:$junitVersion"
testImplementation 'org.assertj:assertj-core:3.11.1'
- testImplementation "org.mockito:mockito-inline:4.0.0"
+ testImplementation "org.mockito:mockito-inline:4.11.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
- testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
- testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
- testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
- testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
- testImplementation 'androidx.test:core:1.3.0'
- testImplementation "androidx.arch.core:core-testing:2.1.0"
+ androidTestImplementation "org.mockito:mockito-android:4.11.0"
+ androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
+ testImplementation "androidx.test:core:$testCoreVersion"
+ testImplementation "androidx.arch.core:core-testing:2.2.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Core library
- androidTestImplementation 'androidx.test:core:1.4.0'
+ androidTestImplementation "androidx.test:core:$testCoreVersion"
+
+ androidTestImplementation('com.adevinta.android:barista:4.2.0') {
+ exclude group: 'org.jetbrains.kotlin'
+ }
// AndroidJUnitRunner and JUnit Rules
- androidTestImplementation 'androidx.test:runner:1.4.0'
- androidTestImplementation 'androidx.test:rules:1.4.0'
+ androidTestImplementation 'androidx.test:runner:1.5.2'
+ androidTestImplementation 'androidx.test:rules:1.5.0'
// Assertions
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.ext:truth:1.4.0'
- androidTestImplementation 'com.google.truth:truth:1.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.ext:truth:1.5.0'
+ testImplementation 'com.google.truth:truth:1.1.3'
+ androidTestImplementation 'com.google.truth:truth:1.1.3'
// Espresso dependencies
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
- androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
- androidTestUtil 'androidx.test:orchestrator:1.4.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
+ androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
+ androidTestUtil 'androidx.test:orchestrator:1.4.2'
testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'org.robolectric:shadows-multidex:4.4'
-}
-def canonicalVersionCode = 309
-def canonicalVersionName = "1.16.0"
+ implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
+ implementation 'androidx.compose.ui:ui:1.5.2'
+ implementation 'androidx.compose.ui:ui-tooling:1.5.2'
+ implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
+ implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
+ implementation "androidx.compose.runtime:runtime-livedata:1.5.2"
-def postFixSize = 10
-def abiPostFix = ['armeabi-v7a' : 1,
- 'arm64-v8a' : 2,
- 'x86' : 3,
- 'x86_64' : 4,
- 'universal' : 5]
-
-android {
- compileSdkVersion androidCompileSdkVersion
- buildToolsVersion '29.0.3'
- useLibrary 'org.apache.http.legacy'
-
- dexOptions {
- javaMaxHeapSize "4g"
- }
-
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-
- kotlinOptions {
- jvmTarget = '1.8'
- }
-
- packagingOptions {
- exclude 'LICENSE.txt'
- exclude 'LICENSE'
- exclude 'NOTICE'
- exclude 'asm-license.txt'
- exclude 'META-INF/LICENSE'
- exclude 'META-INF/NOTICE'
- exclude 'META-INF/proguard/androidx-annotations.pro'
- }
-
- splits {
- abi {
- enable true
- reset()
- include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
- universalApk true
- }
- }
-
- defaultConfig {
- versionCode canonicalVersionCode * postFixSize
- versionName canonicalVersionName
-
- minSdkVersion androidMinimumSdkVersion
- targetSdkVersion androidCompileSdkVersion
-
- multiDexEnabled = true
-
- vectorDrawables.useSupportLibrary = true
- project.ext.set("archivesBaseName", "session")
-
- buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
- buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
- buildConfigField "int", "CONTENT_PROXY_PORT", "443"
- buildConfigField "String", "USER_AGENT", "\"OWA\""
- buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
- buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
-
- resConfigs autoResConfig()
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- // The following argument makes the Android Test Orchestrator run its
- // "pm clear" command after each test invocation. This command ensures
- // that the app's state is completely cleared between tests.
- testInstrumentationRunnerArguments clearPackageData: 'true'
- testOptions {
- execution 'ANDROIDX_TEST_ORCHESTRATOR'
- }
- }
-
- sourceSets {
- String sharedTestDir = 'src/sharedTest/java'
- test.java.srcDirs += sharedTestDir
- androidTest.java.srcDirs += sharedTestDir
- }
-
- buildTypes {
- release {
- minifyEnabled false
- }
- debug {
- minifyEnabled false
- }
- }
-
- flavorDimensions "distribution"
- productFlavors {
- play {
- ext.websiteUpdateUrl = "null"
- buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
- buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
- }
-
- website {
- ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
- buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
- buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
- }
- }
-
- applicationVariants.all { variant ->
- variant.outputs.each { output ->
- def abiName = output.getFilter("ABI") ?: 'universal'
- def postFix = abiPostFix.get(abiName, 0)
-
- if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
- output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
- output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
- }
- }
-
- lintOptions {
- abortOnError true
- baseline file("lint-baseline.xml")
- }
-
- testOptions {
- unitTests {
- includeAndroidResources = true
- }
- }
-
- buildFeatures {
- dataBinding true
- viewBinding true
- }
+ implementation 'androidx.compose.foundation:foundation-layout:1.5.2'
+ implementation 'androidx.compose.material:material:1.5.2'
}
static def getLastCommitTimestamp() {
@@ -310,3 +400,8 @@ def autoResConfig() {
.collect { matcher -> matcher.group(1) }
.sort()
}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+}
diff --git a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
index 087d486893..a20a3a2a67 100644
--- a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
+++ b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
@@ -1,5 +1,6 @@
package network.loki.messenger
+import android.Manifest
import android.app.Instrumentation
import android.content.ClipboardManager
import android.content.Context
@@ -21,6 +22,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.adevinta.android.barista.interaction.PermissionGranter
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
@@ -85,6 +87,8 @@ class HomeActivityTests {
}
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
onView(withId(R.id.registerButton)).perform(ViewActions.click())
+ // allow notification permission
+ PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
}
private fun goToMyChat() {
@@ -100,6 +104,7 @@ class HomeActivityTests {
copied = clipboardManager.primaryClip!!.getItemAt(0).text.toString()
}
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied))
+ onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
}
@@ -153,6 +158,7 @@ class HomeActivityTests {
val dialogPromptText = InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.dialog_open_url_explanation, amazonPuny)
+ onView(isRoot()).perform(waitFor(1000)) // no other way for this to work apparently
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
}
diff --git a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt
new file mode 100644
index 0000000000..157085135e
--- /dev/null
+++ b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt
@@ -0,0 +1,195 @@
+package network.loki.messenger
+
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import network.loki.messenger.libsession_util.ConfigBase
+import network.loki.messenger.libsession_util.Contacts
+import network.loki.messenger.libsession_util.ConversationVolatileConfig
+import network.loki.messenger.libsession_util.util.Contact
+import network.loki.messenger.libsession_util.util.Conversation
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.argWhere
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsignal.utilities.KeyHelper
+import org.session.libsignal.utilities.hexEncodedPublicKey
+import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.crypto.KeyPairUtilities
+import kotlin.random.Random
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LibSessionTests {
+
+ private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
+ private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
+ private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
+
+ private var fakeHashI = 0
+ private val nextFakeHash: String
+ get() = "fakehash${fakeHashI++}"
+
+ private fun maybeGetUserInfo(): Pair? {
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ val prefs = appContext.prefs
+ val localUserPublicKey = prefs.getLocalNumber()
+ val secretKey = with(appContext) {
+ val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
+ edKey.secretKey.asBytes
+ }
+ return if (localUserPublicKey == null || secretKey == null) null
+ else secretKey to localUserPublicKey
+ }
+
+ private fun buildContactMessage(contactList: List): ByteArray {
+ val (key,_) = maybeGetUserInfo()!!
+ val contacts = Contacts.newInstance(key)
+ contactList.forEach { contact ->
+ contacts.set(contact)
+ }
+ return contacts.push().config
+ }
+
+ private fun buildVolatileMessage(conversations: List): ByteArray {
+ val (key, _) = maybeGetUserInfo()!!
+ val volatile = ConversationVolatileConfig.newInstance(key)
+ conversations.forEach { conversation ->
+ volatile.set(conversation)
+ }
+ return volatile.push().config
+ }
+
+ private fun fakePollNewConfig(configBase: ConfigBase, toMerge: ByteArray) {
+ configBase.merge(nextFakeHash to toMerge)
+ MessagingModuleConfiguration.shared.configFactory.persist(configBase, System.currentTimeMillis())
+ }
+
+ @Before
+ fun setupUser() {
+ PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext).edit {
+ putBoolean(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, true).apply()
+ }
+ val newBytes = randomSeedBytes().toByteArray()
+ val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+ val kp = KeyPairUtilities.generate(newBytes)
+ KeyPairUtilities.store(context, kp.seed, kp.ed25519KeyPair, kp.x25519KeyPair)
+ val registrationID = KeyHelper.generateRegistrationId(false)
+ TextSecurePreferences.setLocalRegistrationId(context, registrationID)
+ TextSecurePreferences.setLocalNumber(context, kp.x25519KeyPair.hexEncodedPublicKey)
+ TextSecurePreferences.setRestorationTime(context, 0)
+ TextSecurePreferences.setHasViewedSeed(context, false)
+ }
+
+ @Test
+ fun migration_one_to_ones() {
+ val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ val storageSpy = spy(app.storage)
+ app.storage = storageSpy
+
+ val newContactId = randomSessionId()
+ val singleContact = Contact(
+ id = newContactId,
+ approved = true,
+ expiryMode = ExpiryMode.NONE
+ )
+ val newContactMerge = buildContactMessage(listOf(singleContact))
+ val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
+ fakePollNewConfig(contacts, newContactMerge)
+ verify(storageSpy).addLibSessionContacts(argThat {
+ first().let { it.id == newContactId && it.approved } && size == 1
+ }, any())
+ verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
+ }
+
+ @Test
+ fun test_expected_configs() {
+ val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ val storageSpy = spy(app.storage)
+ app.storage = storageSpy
+
+ val randomRecipient = randomSessionId()
+ val newContact = Contact(
+ id = randomRecipient,
+ approved = true,
+ expiryMode = ExpiryMode.AfterSend(1000)
+ )
+ val newConvo = Conversation.OneToOne(
+ randomRecipient,
+ SnodeAPI.nowWithOffset,
+ false
+ )
+ val volatiles = MessagingModuleConfiguration.shared.configFactory.convoVolatile!!
+ val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
+ val newContactMerge = buildContactMessage(listOf(newContact))
+ val newVolatileMerge = buildVolatileMessage(listOf(newConvo))
+ fakePollNewConfig(contacts, newContactMerge)
+ fakePollNewConfig(volatiles, newVolatileMerge)
+ verify(storageSpy).setExpirationConfiguration(argWhere { config ->
+ config.expiryMode is ExpiryMode.AfterSend
+ && config.expiryMode.expirySeconds == 1000L
+ })
+ val threadId = storageSpy.getThreadId(Address.fromSerialized(randomRecipient))!!
+ val newExpiry = storageSpy.getExpirationConfiguration(threadId)!!
+ assertThat(newExpiry.expiryMode, instanceOf(ExpiryMode.AfterSend::class.java))
+ assertThat(newExpiry.expiryMode.expirySeconds, equalTo(1000))
+ assertThat(newExpiry.expiryMode.expiryMillis, equalTo(1000000))
+ }
+
+ @Test
+ fun test_overwrite_config() {
+ val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ val storageSpy = spy(app.storage)
+ app.storage = storageSpy
+
+ // Initial state
+ val randomRecipient = randomSessionId()
+ val currentContact = Contact(
+ id = randomRecipient,
+ approved = true,
+ expiryMode = ExpiryMode.NONE
+ )
+ val newConvo = Conversation.OneToOne(
+ randomRecipient,
+ SnodeAPI.nowWithOffset,
+ false
+ )
+ val volatiles = MessagingModuleConfiguration.shared.configFactory.convoVolatile!!
+ val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
+ val newContactMerge = buildContactMessage(listOf(currentContact))
+ val newVolatileMerge = buildVolatileMessage(listOf(newConvo))
+ fakePollNewConfig(contacts, newContactMerge)
+ fakePollNewConfig(volatiles, newVolatileMerge)
+ verify(storageSpy).setExpirationConfiguration(argWhere { config ->
+ config.expiryMode == ExpiryMode.NONE
+ })
+ val threadId = storageSpy.getThreadId(Address.fromSerialized(randomRecipient))!!
+ val currentExpiryConfig = storageSpy.getExpirationConfiguration(threadId)!!
+ assertThat(currentExpiryConfig.expiryMode, equalTo(ExpiryMode.NONE))
+ assertThat(currentExpiryConfig.expiryMode.expirySeconds, equalTo(0))
+ assertThat(currentExpiryConfig.expiryMode.expiryMillis, equalTo(0))
+ // Set new state and overwrite
+ val updatedContact = currentContact.copy(expiryMode = ExpiryMode.AfterSend(1000))
+ val updateContactMerge = buildContactMessage(listOf(updatedContact))
+ fakePollNewConfig(contacts, updateContactMerge)
+ val updatedExpiryConfig = storageSpy.getExpirationConfiguration(threadId)!!
+ assertThat(updatedExpiryConfig.expiryMode, instanceOf(ExpiryMode.AfterSend::class.java))
+ assertThat(updatedExpiryConfig.expiryMode.expirySeconds, equalTo(1000))
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/huawei/AndroidManifest.xml b/app/src/huawei/AndroidManifest.xml
new file mode 100644
index 0000000000..dad7ab3ac6
--- /dev/null
+++ b/app/src/huawei/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/huawei/agconnect-services.json b/app/src/huawei/agconnect-services.json
new file mode 100644
index 0000000000..0c81d0477a
--- /dev/null
+++ b/app/src/huawei/agconnect-services.json
@@ -0,0 +1,96 @@
+{
+ "agcgw":{
+ "backurl":"connect-dre.hispace.hicloud.com",
+ "url":"connect-dre.dbankcloud.cn",
+ "websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com",
+ "websocketurl":"connect-ws-dre.hispace.dbankcloud.cn"
+ },
+ "agcgw_all":{
+ "CN":"connect-drcn.dbankcloud.cn",
+ "CN_back":"connect-drcn.hispace.hicloud.com",
+ "DE":"connect-dre.dbankcloud.cn",
+ "DE_back":"connect-dre.hispace.hicloud.com",
+ "RU":"connect-drru.hispace.dbankcloud.ru",
+ "RU_back":"connect-drru.hispace.dbankcloud.cn",
+ "SG":"connect-dra.dbankcloud.cn",
+ "SG_back":"connect-dra.hispace.hicloud.com"
+ },
+ "websocketgw_all":{
+ "CN":"connect-ws-drcn.hispace.dbankcloud.cn",
+ "CN_back":"connect-ws-drcn.hispace.dbankcloud.com",
+ "DE":"connect-ws-dre.hispace.dbankcloud.cn",
+ "DE_back":"connect-ws-dre.hispace.dbankcloud.com",
+ "RU":"connect-ws-drru.hispace.dbankcloud.ru",
+ "RU_back":"connect-ws-drru.hispace.dbankcloud.cn",
+ "SG":"connect-ws-dra.hispace.dbankcloud.cn",
+ "SG_back":"connect-ws-dra.hispace.dbankcloud.com"
+ },
+ "client":{
+ "cp_id":"890061000023000573",
+ "product_id":"99536292102532562",
+ "client_id":"954244311350791232",
+ "client_secret":"555999202D718B6744DAD2E923B386DC17F3F4E29F5105CE0D061EED328DADEE",
+ "project_id":"99536292102532562",
+ "app_id":"107205081",
+ "api_key":"DAEDABeddLEqUy0QRwa1THLwRA0OqrSuyci/HjNvVSmsdWsXRM2U2hRaCyqfvGYH1IFOKrauArssz/WPMLRHCYxliWf+DTj9bDwlWA==",
+ "package_name":"network.loki.messenger"
+ },
+ "oauth_client":{
+ "client_id":"107205081",
+ "client_type":1
+ },
+ "app_info":{
+ "app_id":"107205081",
+ "package_name":"network.loki.messenger"
+ },
+ "service":{
+ "analytics":{
+ "collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
+ "collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
+ "collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
+ "collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
+ "collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
+ "resource_id":"p1",
+ "channel_id":""
+ },
+ "edukit":{
+ "edu_url":"edukit.edu.cloud.huawei.com.cn",
+ "dh_url":"edukit.edu.cloud.huawei.com.cn"
+ },
+ "search":{
+ "url":"https://search-dre.cloud.huawei.com"
+ },
+ "cloudstorage":{
+ "storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia",
+ "storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru",
+ "storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru",
+ "storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu",
+ "storage_url_de":"https://ops-dre.agcstorage.link",
+ "storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn",
+ "storage_url_sg":"https://ops-dra.agcstorage.link",
+ "storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn",
+ "storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn"
+ },
+ "ml":{
+ "mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
+ }
+ },
+ "region":"DE",
+ "configuration_version":"3.0",
+ "appInfos":[
+ {
+ "package_name":"network.loki.messenger",
+ "client":{
+ "app_id":"107205081"
+ },
+ "app_info":{
+ "package_name":"network.loki.messenger",
+ "app_id":"107205081"
+ },
+ "oauth_client":{
+ "client_type":1,
+ "client_id":"107205081"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt
new file mode 100644
index 0000000000..26a484df16
--- /dev/null
+++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt
@@ -0,0 +1,13 @@
+package org.thoughtcrime.securesms.notifications
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class HuaweiBindingModule {
+ @Binds
+ abstract fun bindTokenFetcher(tokenFetcher: HuaweiTokenFetcher): TokenFetcher
+}
diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt
new file mode 100644
index 0000000000..dc7bf893d7
--- /dev/null
+++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt
@@ -0,0 +1,40 @@
+package org.thoughtcrime.securesms.notifications
+
+import android.os.Bundle
+import com.huawei.hms.push.HmsMessageService
+import com.huawei.hms.push.RemoteMessage
+import dagger.hilt.android.AndroidEntryPoint
+import org.json.JSONException
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsignal.utilities.Base64
+import org.session.libsignal.utilities.Log
+import java.lang.Exception
+import javax.inject.Inject
+
+private val TAG = HuaweiPushService::class.java.simpleName
+
+@AndroidEntryPoint
+class HuaweiPushService: HmsMessageService() {
+ @Inject lateinit var pushRegistry: PushRegistry
+ @Inject lateinit var pushReceiver: PushReceiver
+
+ override fun onMessageReceived(message: RemoteMessage?) {
+ Log.d(TAG, "onMessageReceived")
+ message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPush) ?:
+ pushReceiver.onPush(message?.data?.let(Base64::decode))
+ }
+
+ override fun onNewToken(token: String?) {
+ pushRegistry.register(token)
+ }
+
+ override fun onNewToken(token: String?, bundle: Bundle?) {
+ Log.d(TAG, "New HCM token: $token.")
+ pushRegistry.register(token)
+ }
+
+ override fun onDeletedMessages() {
+ Log.d(TAG, "onDeletedMessages")
+ pushRegistry.refresh(false)
+ }
+}
diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt
new file mode 100644
index 0000000000..9d9b61ce9a
--- /dev/null
+++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt
@@ -0,0 +1,29 @@
+package org.thoughtcrime.securesms.notifications
+
+import android.content.Context
+import com.huawei.hms.aaid.HmsInstanceId
+import dagger.Lazy
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.session.libsignal.utilities.Log
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val APP_ID = "107205081"
+private const val TOKEN_SCOPE = "HCM"
+
+@Singleton
+class HuaweiTokenFetcher @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val pushRegistry: Lazy,
+): TokenFetcher {
+ override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run {
+ // https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/push-basic-capability#h2-1576218800370
+ // getToken may return an empty string, if so HuaweiPushService#onNewToken will be called.
+ withContext(Dispatchers.IO) { getToken(APP_ID, TOKEN_SCOPE) }
+ }
+}
diff --git a/app/src/huawei/res/values/strings.xml b/app/src/huawei/res/values/strings.xml
new file mode 100644
index 0000000000..78d42b3e30
--- /dev/null
+++ b/app/src/huawei/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers.
+ You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers.
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 681fc00c17..79d55b37f8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,8 +1,7 @@
+ xmlns:tools="http://schemas.android.com/tools">
@@ -30,11 +29,19 @@
android:name="android.hardware.touchscreen"
android:required="false" />
+
+
+
+
+
+
+
+
@@ -100,11 +107,6 @@
android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
-
+
+ android:theme="@style/Theme.Session.DayNight.NoActionBar"
+ android:windowSoftInputMode="adjustResize" >
+
-
-
-
-
-
-
+ android:exported="false" android:foregroundServiceType="specialUse">
+
+
+
@@ -398,57 +399,49 @@
android:authorities="network.loki.securesms.database.recipient"
android:exported="false" />
-
+
-
+
-
-
-
-
-
-
+
-
+
+ android:exported="false">
+ android:enabled="true"
+ android:exported="true">
-
-
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
index 01bc1f38ae..03b56d6b61 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
@@ -40,6 +40,8 @@ import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPol
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.Address;
+import org.session.libsession.utilities.ConfigFactoryUpdateListener;
+import org.session.libsession.utilities.Device;
import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
@@ -47,6 +49,7 @@ import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.WindowDebouncer;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
+import org.session.libsignal.utilities.HTTP;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils;
@@ -54,33 +57,30 @@ import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
-import org.thoughtcrime.securesms.database.JobDatabase;
+import org.thoughtcrime.securesms.database.LastSentTimestampCache;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.Storage;
+import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
+import org.thoughtcrime.securesms.dependencies.AppComponent;
+import org.thoughtcrime.securesms.dependencies.ConfigFactory;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.groups.OpenGroupManager;
-import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
import org.thoughtcrime.securesms.home.HomeActivity;
-import org.thoughtcrime.securesms.jobmanager.JobManager;
-import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
-import org.thoughtcrime.securesms.jobs.FastJobStorage;
-import org.thoughtcrime.securesms.jobs.JobManagerFactories;
+import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
-import org.thoughtcrime.securesms.notifications.FcmUtils;
-import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
+import org.thoughtcrime.securesms.notifications.PushRegistry;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService;
-import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
@@ -111,6 +111,8 @@ import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit;
import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig;
+import network.loki.messenger.libsession_util.ConfigBase;
+import network.loki.messenger.libsession_util.UserProfile;
/**
* Will be called once when the TextSecure process is created.
@@ -121,7 +123,7 @@ import network.loki.messenger.BuildConfig;
* @author Moxie Marlinspike
*/
@HiltAndroidApp
-public class ApplicationContext extends Application implements DefaultLifecycleObserver {
+public class ApplicationContext extends Application implements DefaultLifecycleObserver, ConfigFactoryUpdateListener {
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
@@ -130,7 +132,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private ExpiringMessageManager expiringMessageManager;
private TypingStatusRepository typingStatusRepository;
private TypingStatusSender typingStatusSender;
- private JobManager jobManager;
private ReadReceiptManager readReceiptManager;
private ProfileManager profileManager;
public MessageNotifier messageNotifier = null;
@@ -143,10 +144,13 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private PersistentLogger persistentLogger;
@Inject LokiAPIDatabase lokiAPIDatabase;
- @Inject Storage storage;
+ @Inject public Storage storage;
+ @Inject Device device;
@Inject MessageDataProvider messageDataProvider;
- @Inject JobDatabase jobDatabase;
@Inject TextSecurePreferences textSecurePreferences;
+ @Inject PushRegistry pushRegistry;
+ @Inject ConfigFactory configFactory;
+ @Inject LastSentTimestampCache lastSentTimestampCache;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@@ -165,7 +169,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
public TextSecurePreferences getPrefs() {
- return textSecurePreferences;
+ return EntryPoints.get(getApplicationContext(), AppComponent.class).getPrefs();
}
public DatabaseComponent getDatabaseComponent() {
@@ -194,18 +198,31 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return this.persistentLogger;
}
+ @Override
+ public void notifyUpdates(@NonNull ConfigBase forConfigObject, long messageTimestamp) {
+ // forward to the config factory / storage ig
+ if (forConfigObject instanceof UserProfile && !textSecurePreferences.getConfigurationMessageSynced()) {
+ textSecurePreferences.setConfigurationMessageSynced(true);
+ }
+ storage.notifyConfigUpdates(forConfigObject, messageTimestamp);
+ }
+
@Override
public void onCreate() {
+ TextSecurePreferences.setPushSuffix(BuildConfig.PUSH_KEY_SUFFIX);
+
DatabaseModule.init(this);
MessagingModuleConfiguration.configure(this);
super.onCreate();
- messagingModuleConfiguration = new MessagingModuleConfiguration(this,
+ messagingModuleConfiguration = new MessagingModuleConfiguration(
+ this,
storage,
+ device,
messageDataProvider,
- ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
- // migrate session open group data
- OpenGroupMigrator.migrate(getDatabaseComponent());
- // end migration
+ ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
+ configFactory,
+ lastSentTimestampCache
+ );
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()");
startKovenant();
@@ -219,10 +236,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
broadcaster = new Broadcaster(this);
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
SnodeModule.Companion.configure(apiDB, broadcaster);
- String userPublicKey = TextSecurePreferences.getLocalNumber(this);
- if (userPublicKey != null) {
- registerForFCMIfNeeded(false);
- }
initializeExpiringMessageManager();
initializeTypingStatusRepository();
initializeTypingStatusSender();
@@ -230,12 +243,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
initializeProfileManager();
initializePeriodicTasks();
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
- initializeJobManager();
initializeWebRtc();
initializeBlobProvider();
resubmitProfilePictureIfNeeded();
loadEmojiSearchIndexIfNeeded();
EmojiSource.refresh();
+
+ NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
+ HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
}
@Override
@@ -244,6 +259,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
Log.i(TAG, "App is now visible.");
KeyCachingService.onAppForegrounded(this);
+ // If the user account hasn't been created or onboarding wasn't finished then don't start
+ // the pollers
+ if (TextSecurePreferences.getLocalNumber(this) == null || !TextSecurePreferences.hasSeenWelcomeScreen(this)) {
+ return;
+ }
+
ThreadUtils.queue(()->{
if (poller != null) {
poller.setCaughtUp(false);
@@ -264,7 +285,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
if (poller != null) {
poller.stopIfNeeded();
}
- ClosedGroupPollerV2.getShared().stop();
+ ClosedGroupPollerV2.getShared().stopAll();
}
@Override
@@ -278,10 +299,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
LocaleParser.Companion.configure(new LocaleParseHelper());
}
- public JobManager getJobManager() {
- return jobManager;
- }
-
public ExpiringMessageManager getExpiringMessageManager() {
return expiringMessageManager;
}
@@ -344,16 +361,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
}
- private void initializeJobManager() {
- this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
- .setDataSerializer(new JsonDataSerializer())
- .setJobFactories(JobManagerFactories.getJobFactories(this))
- .setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
- .setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
- .setJobStorage(new FastJobStorage(jobDatabase))
- .build());
- }
-
private void initializeExpiringMessageManager() {
this.expiringMessageManager = new ExpiringMessageManager(this);
}
@@ -367,7 +374,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
private void initializeProfileManager() {
- this.profileManager = new ProfileManager();
+ this.profileManager = new ProfileManager(this, configFactory);
}
private void initializeTypingStatusSender() {
@@ -376,10 +383,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private void initializePeriodicTasks() {
BackgroundPollWorker.schedulePeriodic(this);
-
- if (BuildConfig.PLAY_STORE_DISABLED) {
- UpdateApkRefreshListener.schedule(this);
- }
}
private void initializeWebRtc() {
@@ -430,29 +433,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
private static class ProviderInitializationException extends RuntimeException { }
-
- public void registerForFCMIfNeeded(final Boolean force) {
- if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
- if (force && firebaseInstanceIdJob != null) {
- firebaseInstanceIdJob.cancel(null);
- }
- firebaseInstanceIdJob = FcmUtils.getFcmInstanceId(task->{
- if (!task.isSuccessful()) {
- Log.w("Loki", "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.getException());
- return Unit.INSTANCE;
- }
- String token = task.getResult().getToken();
- String userPublicKey = TextSecurePreferences.getLocalNumber(this);
- if (userPublicKey == null) return Unit.INSTANCE;
- if (TextSecurePreferences.isUsingFCM(this)) {
- LokiPushNotificationManager.register(token, userPublicKey, this, force);
- } else {
- LokiPushNotificationManager.unregister(token, this);
- }
- return Unit.INSTANCE;
- });
- }
-
private void setUpPollingIfNeeded() {
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return;
@@ -460,7 +440,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.setUserPublicKey(userPublicKey);
return;
}
- poller = new Poller();
+ poller = new Poller(configFactory, new Timer());
}
public void startPollingIfNeeded() {
@@ -481,6 +461,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
ThreadUtils.queue(() -> {
// Don't generate a new profile key here; we do that when the user changes their profile picture
+ Log.d("Loki-Avatar", "Uploading Avatar Started");
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
try {
// Read the file into a byte array
@@ -497,10 +478,11 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
// Update the last profile picture upload date
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
+ Log.d("Loki-Avatar", "Uploading Avatar Finished");
return Unit.INSTANCE;
});
- } catch (Exception exception) {
- // Do nothing
+ } catch (Exception e) {
+ Log.e("Loki-Avatar", "Uploading avatar failed.");
}
});
}
@@ -520,24 +502,21 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
public void clearAllData(boolean isMigratingToV2KeyPair) {
- String token = TextSecurePreferences.getFCMToken(this);
- if (token != null && !token.isEmpty()) {
- LokiPushNotificationManager.unregister(token, this);
- }
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
firebaseInstanceIdJob.cancel(null);
}
String displayName = TextSecurePreferences.getProfileName(this);
- boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
+ boolean isUsingFCM = TextSecurePreferences.isPushEnabled(this);
TextSecurePreferences.clearAll(this);
if (isMigratingToV2KeyPair) {
- TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
+ TextSecurePreferences.setPushEnabled(this, isUsingFCM);
TextSecurePreferences.setProfileName(this, displayName);
}
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
- if (!deleteDatabase("signal.db")) {
+ if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
Log.d("Loki", "Failed to delete database.");
}
+ configFactory.keyPairChanged();
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
index 7d82c760cc..a99fe83430 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms;
+import static android.os.Build.VERSION.SDK_INT;
import static org.session.libsession.utilities.TextSecurePreferences.SELECTED_ACCENT_COLOR;
import android.app.ActivityManager;
@@ -18,6 +19,7 @@ import androidx.appcompat.app.AppCompatActivity;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
+import org.thoughtcrime.securesms.conversation.v2.WindowUtil;
import org.thoughtcrime.securesms.util.ActivityUtilitiesKt;
import org.thoughtcrime.securesms.util.ThemeState;
import org.thoughtcrime.securesms.util.UiModeUtilities;
@@ -28,30 +30,37 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
public ThemeState currentThemeState;
+ private Resources.Theme modifiedTheme;
+
private TextSecurePreferences getPreferences() {
ApplicationContext appContext = (ApplicationContext) getApplicationContext();
return appContext.textSecurePreferences;
}
@StyleRes
- public int getDesiredTheme() {
+ private int getDesiredTheme() {
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
int userSelectedTheme = themeState.getTheme();
+
+ // If the user has configured Session to follow the system light/dark theme mode then do so..
if (themeState.getFollowSystem()) {
- // do light or dark based on the selected theme
+
+ // Use light or dark versions of the user's theme based on light-mode / dark-mode settings
boolean isDayUi = UiModeUtilities.isDayUiMode(this);
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
} else {
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
}
- } else {
+ }
+ else // ..otherwise just return their selected theme.
+ {
return userSelectedTheme;
}
}
@StyleRes @Nullable
- public Integer getAccentTheme() {
+ private Integer getAccentTheme() {
if (!getPreferences().hasPreference(SELECTED_ACCENT_COLOR)) return null;
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
return themeState.getAccentStyle();
@@ -59,8 +68,12 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
@Override
public Resources.Theme getTheme() {
+ if (modifiedTheme != null) {
+ return modifiedTheme;
+ }
+
// New themes
- Resources.Theme modifiedTheme = super.getTheme();
+ modifiedTheme = super.getTheme();
modifiedTheme.applyStyle(getDesiredTheme(), true);
Integer accentTheme = getAccentTheme();
if (accentTheme != null) {
@@ -92,6 +105,11 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
if (!currentThemeState.equals(ActivityUtilitiesKt.themeState(getPreferences()))) {
recreate();
}
+
+ // apply lightStatusBar manually as API 26 does not update properly via applyTheme
+ // https://issuetracker.google.com/issues/65883460?pli=1
+ if (SDK_INT >= 26 && SDK_INT <= 27) WindowUtil.setLightStatusBarFromTheme(this);
+ if (SDK_INT == 27) WindowUtil.setLightNavigationBarFromTheme(this);
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
deleted file mode 100644
index 93313e5270..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-
-import org.thoughtcrime.securesms.database.model.MessageRecord;
-import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
-import org.thoughtcrime.securesms.mms.GlideRequests;
-import org.session.libsignal.utilities.guava.Optional;
-
-import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
-import org.session.libsession.utilities.Address;
-import org.session.libsession.utilities.recipients.Recipient;
-
-import java.util.Locale;
-import java.util.Set;
-
-public interface BindableConversationItem extends Unbindable {
- void bind(@NonNull MessageRecord messageRecord,
- @NonNull Optional previousMessageRecord,
- @NonNull Optional nextMessageRecord,
- @NonNull GlideRequests glideRequests,
- @NonNull Locale locale,
- @NonNull Set batchSelected,
- @NonNull Recipient recipients,
- @Nullable String searchQuery,
- boolean pulseHighlight);
-
- MessageRecord getMessageRecord();
-
- void setEventListener(@Nullable EventListener listener);
-
- interface EventListener {
- void onQuoteClicked(MmsMessageRecord messageRecord);
- void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
- void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt
new file mode 100644
index 0000000000..af38c31ff3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt
@@ -0,0 +1,28 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import network.loki.messenger.R
+
+class DeleteMediaDialog {
+ companion object {
+ @JvmStatic
+ fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
+ iconAttribute(R.attr.dialog_alert_icon)
+ title(
+ context.resources.getQuantityString(
+ R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
+ recordCount,
+ recordCount
+ )
+ )
+ text(
+ context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
+ recordCount,
+ recordCount
+ )
+ )
+ button(R.string.delete) { doDelete.run() }
+ cancelButton()
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt
new file mode 100644
index 0000000000..0390a3007d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt
@@ -0,0 +1,19 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import network.loki.messenger.R
+
+class DeleteMediaPreviewDialog {
+ companion object {
+ @JvmStatic
+ fun show(context: Context, doDelete: Runnable) {
+ context.showSessionDialog {
+ iconAttribute(R.attr.dialog_alert_icon)
+ title(R.string.MediaPreviewActivity_media_delete_confirmation_title)
+ text(R.string.MediaPreviewActivity_media_delete_confirmation_message)
+ button(R.string.delete) { doDelete.run() }
+ cancelButton()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt b/app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt
new file mode 100644
index 0000000000..bdfa9b6088
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt
@@ -0,0 +1,16 @@
+package org.thoughtcrime.securesms
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import network.loki.messenger.BuildConfig
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DeviceModule {
+ @Provides
+ @Singleton
+ fun provides() = BuildConfig.DEVICE
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java b/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java
deleted file mode 100644
index 469629ed3f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.TextView;
-
-import org.session.libsession.utilities.ExpirationUtil;
-
-import cn.carbswang.android.numberpickerview.library.NumberPickerView;
-import network.loki.messenger.R;
-
-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(context.getResources().getIntArray(R.array.expiration_times)[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 = context.getResources().getIntArray(R.array.expiration_times);
- final String[] expirationDisplayValues = new String[expirationTimes.length];
-
- int selectedIndex = expirationTimes.length - 1;
-
- for (int i=0;i= 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;
- }
-
- public interface OnClickListener {
- public void onClick(int expirationTime);
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java
index aad4c17008..0fd813cf4b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java
@@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
if (slide != null) {
- thumbnailView.setImageResource(glideRequests, slide, false, false);
+ thumbnailView.setImageResource(glideRequests, slide, false, null);
}
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java
index 53a909c5ac..95ba15c82e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java
@@ -54,6 +54,7 @@ import com.google.android.material.tabs.TabLayout;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
+import org.session.libsession.snode.SnodeAPI;
import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MediaDatabase;
@@ -75,6 +76,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
+import kotlin.Unit;
import network.loki.messenger.R;
/**
@@ -317,9 +319,9 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
@SuppressWarnings("CodeBlock2Expr")
@SuppressLint({"InlinedApi", "StaticFieldLeak"})
private void handleSaveMedia(@NonNull Collection mediaRecords) {
- final Context context = getContext();
+ final Context context = requireContext();
- SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> {
+ SaveAttachmentTask.showWarningDialog(context, mediaRecords.size(), () -> {
Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
@@ -361,53 +363,39 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
}.execute();
})
.execute();
- }, mediaRecords.size());
+ return Unit.INSTANCE;
+ });
}
private void sendMediaSavedNotificationIfNeeded() {
if (recipient.isGroupRecipient()) return;
- DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
+ DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
MessageSender.send(message, recipient.getAddress());
}
@SuppressLint("StaticFieldLeak")
private void handleDeleteMedia(@NonNull Collection mediaRecords) {
int recordCount = mediaRecords.size();
- Resources res = getContext().getResources();
- String confirmTitle = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
- recordCount,
- recordCount);
- String confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
- recordCount,
- recordCount);
- AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
- builder.setIconAttribute(R.attr.dialog_alert_icon);
- builder.setTitle(confirmTitle);
- builder.setMessage(confirmMessage);
- builder.setCancelable(true);
-
- builder.setPositiveButton(R.string.delete, (dialogInterface, i) -> {
- new ProgressDialogAsyncTask(getContext(),
- R.string.MediaOverviewActivity_Media_delete_progress_title,
- R.string.MediaOverviewActivity_Media_delete_progress_message)
- {
- @Override
- protected Void doInBackground(MediaDatabase.MediaRecord... records) {
- if (records == null || records.length == 0) {
- return null;
- }
-
- for (MediaDatabase.MediaRecord record : records) {
- AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
- }
+ DeleteMediaDialog.show(
+ requireContext(),
+ recordCount,
+ () -> new ProgressDialogAsyncTask(
+ requireContext(),
+ R.string.MediaOverviewActivity_Media_delete_progress_title,
+ R.string.MediaOverviewActivity_Media_delete_progress_message) {
+ @Override
+ protected Void doInBackground(MediaDatabase.MediaRecord... records) {
+ if (records == null || records.length == 0) {
return null;
}
- }.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()]));
- });
- builder.setNegativeButton(android.R.string.cancel, null);
- builder.show();
+ for (MediaDatabase.MediaRecord record : records) {
+ AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
+ }
+ return null;
+ }
+ }.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()])));
}
private void handleSelectAllMedia() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
index b21c7dac8c..2e67becbfd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -21,6 +21,7 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
+import android.database.CursorIndexOutOfBoundsException;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@@ -47,7 +48,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Pair;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
@@ -60,6 +60,7 @@ import androidx.viewpager.widget.ViewPager;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
+import org.session.libsession.snode.SnodeAPI;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
@@ -84,6 +85,7 @@ import java.io.IOException;
import java.util.Locale;
import java.util.WeakHashMap;
+import kotlin.Unit;
import network.loki.messenger.R;
/**
@@ -144,6 +146,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}
};
+ private MediaItemAdapter adapter;
+
+ public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) {
+ return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread());
+ }
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
Intent previewIntent = null;
@@ -212,13 +219,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
- @TargetApi(VERSION_CODES.JELLY_BEAN)
- private void setFullscreenIfPossible() {
- if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
- }
- }
-
@Override
public void onModified(Recipient recipient) {
Util.runOnMain(this::updateActionBar);
@@ -280,9 +280,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
mediaPager = findViewById(R.id.media_pager);
mediaPager.setOffscreenPageLimit(1);
- viewPagerListener = new ViewPagerListener();
- mediaPager.addOnPageChangeListener(viewPagerListener);
-
albumRail = findViewById(R.id.media_preview_album_rail);
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
@@ -373,7 +370,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (conversationRecipient != null) {
getSupportLoaderManager().restartLoader(0, null, this);
} else {
- mediaPager.setAdapter(new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize));
+ adapter = new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize);
+ mediaPager.setAdapter(adapter);
if (initialCaption != null) {
detailsContainer.setVisibility(View.VISIBLE);
@@ -415,7 +413,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem == null) return;
- SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
+ SaveAttachmentTask.showWarningDialog(this, 1, () -> {
Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
@@ -423,7 +421,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
- long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
+ long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
saveTask.executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR,
new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
@@ -432,12 +430,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
})
.execute();
+ return Unit.INSTANCE;
});
}
private void sendMediaSavedNotificationIfNeeded() {
if (conversationRecipient.isGroupRecipient()) return;
- DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
+ DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
MessageSender.send(message, conversationRecipient.getAddress());
}
@@ -448,29 +447,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
return;
}
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setIconAttribute(R.attr.dialog_alert_icon);
- builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
- builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
- builder.setCancelable(true);
-
- builder.setPositiveButton(R.string.delete, (dialogInterface, which) -> {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... voids) {
- if (mediaItem.attachment == null) {
- return null;
- }
- AttachmentUtil.deleteAttachment(MediaPreviewActivity.this.getApplicationContext(),
- mediaItem.attachment);
- return null;
- }
- }.execute();
+ DeleteMediaPreviewDialog.show(this, () -> {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... voids) {
+ DatabaseAttachment attachment = mediaItem.attachment;
+ if (attachment != null) {
+ AttachmentUtil.deleteAttachment(getApplicationContext(), attachment);
+ }
+ return null;
+ }
+ }.execute();
finish();
});
- builder.setNegativeButton(android.R.string.cancel, null);
- builder.show();
}
@Override
@@ -509,13 +499,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
private @Nullable MediaItem getCurrentMediaItem() {
- MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
-
- if (adapter != null) {
- return adapter.getMediaItemFor(mediaPager.getCurrentItem());
- } else {
- return null;
- }
+ if (adapter == null) return null;
+ return adapter.getMediaItemFor(mediaPager.getCurrentItem());
}
public static boolean isContentTypeSupported(final String contentType) {
@@ -529,20 +514,28 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override
public void onLoadFinished(@NonNull Loader> loader, @Nullable Pair data) {
- if (data != null) {
- @SuppressWarnings("ConstantConditions")
- CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
- mediaPager.setAdapter(adapter);
- adapter.setActive(true);
+ if (data == null) return;
- viewModel.setCursor(this, data.first, leftIsRecent);
+ mediaPager.removeOnPageChangeListener(viewPagerListener);
- int item = restartItem >= 0 ? restartItem : data.second;
+ adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
+ mediaPager.setAdapter(adapter);
+
+ viewModel.setCursor(this, data.first, leftIsRecent);
+
+ int item = restartItem >= 0 && restartItem < adapter.getCount() ? restartItem : Math.max(Math.min(data.second, adapter.getCount() - 1), 0);
+
+ viewPagerListener = new ViewPagerListener();
+ mediaPager.addOnPageChangeListener(viewPagerListener);
+
+ try {
mediaPager.setCurrentItem(item);
+ } catch (CursorIndexOutOfBoundsException e) {
+ throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
+ }
- if (item == 0) {
- viewPagerListener.onPageSelected(0);
- }
+ if (item == 0) {
+ viewPagerListener.onPageSelected(0);
}
}
@@ -560,26 +553,26 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (currentPage != -1 && currentPage != position) onPageUnselected(currentPage);
currentPage = position;
- MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
+ if (adapter == null) return;
- if (adapter != null) {
- MediaItem item = adapter.getMediaItemFor(position);
- if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this);
- viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
- updateActionBar();
- }
+ MediaItem item = adapter.getMediaItemFor(position);
+ if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this);
+ viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
+ updateActionBar();
}
public void onPageUnselected(int position) {
- MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
+ if (adapter == null) return;
- if (adapter != null) {
+ try {
MediaItem item = adapter.getMediaItemFor(position);
if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this);
-
- adapter.pause(position);
+ } catch (CursorIndexOutOfBoundsException e) {
+ throw new RuntimeException("position = " + position + " leftIsRecent = " + leftIsRecent, e);
}
+
+ adapter.pause(position);
}
@Override
@@ -593,7 +586,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
}
- private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter {
+ private static class SingleItemPagerAdapter extends MediaItemAdapter {
private final GlideRequests glideRequests;
private final Window window;
@@ -665,7 +658,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
}
- private static class CursorPagerAdapter extends PagerAdapter implements MediaItemAdapter {
+ private static class CursorPagerAdapter extends MediaItemAdapter {
private final WeakHashMap mediaViews = new WeakHashMap<>();
@@ -675,7 +668,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private final Cursor cursor;
private final boolean leftIsRecent;
- private boolean active;
private int autoPlayPosition;
CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
@@ -690,15 +682,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
this.leftIsRecent = leftIsRecent;
}
- public void setActive(boolean active) {
- this.active = active;
- notifyDataSetChanged();
- }
-
@Override
public int getCount() {
- if (!active) return 0;
- else return cursor.getCount();
+ return cursor.getCount();
}
@Override
@@ -771,8 +757,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
private int getCursorPosition(int position) {
- if (leftIsRecent) return position;
- else return cursor.getCount() - 1 - position;
+ int unclamped = leftIsRecent ? position : cursor.getCount() - 1 - position;
+ return Math.max(Math.min(unclamped, cursor.getCount() - 1), 0);
}
}
@@ -800,9 +786,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
}
- interface MediaItemAdapter {
- MediaItem getMediaItemFor(int position);
- void pause(int position);
- @Nullable View getPlaybackControls(int position);
+ abstract static class MediaItemAdapter extends PagerAdapter {
+ abstract MediaItem getMediaItemFor(int position);
+ abstract void pause(int position);
+ @Nullable abstract View getPlaybackControls(int position);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt
new file mode 100644
index 0000000000..00e2c3d6d8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt
@@ -0,0 +1,11 @@
+package org.thoughtcrime.securesms
+
+import org.session.libsession.utilities.recipients.Recipient
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord
+import org.thoughtcrime.securesms.mms.Slide
+
+data class MediaPreviewArgs(
+ val slide: Slide,
+ val mmsRecord: MmsMessageRecord?,
+ val thread: Recipient?,
+)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java
deleted file mode 100644
index ca6cf8f6c8..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java
+++ /dev/null
@@ -1,111 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import android.content.Context;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.BaseAdapter;
-
-import androidx.annotation.NonNull;
-
-
-import org.thoughtcrime.securesms.database.model.MessageRecord;
-import org.thoughtcrime.securesms.contacts.UserView;
-import org.thoughtcrime.securesms.mms.GlideRequests;
-import org.session.libsession.utilities.recipients.Recipient;
-import org.session.libsession.utilities.Conversions;
-
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.List;
-
-class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
-
- private final Context context;
- private final GlideRequests glideRequests;
- private final MessageRecord record;
- private final List members;
- private final boolean isPushGroup;
-
- MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
- @NonNull MessageRecord record, @NonNull List members,
- boolean isPushGroup)
- {
- this.context = context;
- this.glideRequests = glideRequests;
- this.record = record;
- this.isPushGroup = isPushGroup;
- this.members = members;
- }
-
- @Override
- public int getCount() {
- return members.size();
- }
-
- @Override
- public Object getItem(int position) {
- return members.get(position);
- }
-
- @Override
- public long getItemId(int position) {
- try {
- return Conversions.byteArrayToLong(MessageDigest.getInstance("SHA1").digest(members.get(position).recipient.getAddress().serialize().getBytes()));
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e);
- }
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- UserView result = new UserView(context);
- Recipient recipient = members.get(position).getRecipient();
- result.setOpenGroupThreadID(record.getThreadId());
- result.bind(recipient, glideRequests, UserView.ActionIndicator.None, false);
- return result;
- }
-
- @Override
- public void onMovedToScrapHeap(View view) {
- ((UserView)view).unbind();
- }
-
-
- static class RecipientDeliveryStatus {
-
- enum Status {
- UNKNOWN, PENDING, SENT, DELIVERED, READ
- }
-
- private final Recipient recipient;
- private final Status deliveryStatus;
- private final boolean isUnidentified;
- private final long timestamp;
-
- RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) {
- this.recipient = recipient;
- this.deliveryStatus = deliveryStatus;
- this.isUnidentified = isUnidentified;
- this.timestamp = timestamp;
- }
-
- Status getDeliveryStatus() {
- return deliveryStatus;
- }
-
- boolean isUnidentified() {
- return isUnidentified;
- }
-
- public long getTimestamp() {
- return timestamp;
- }
-
- public Recipient getRecipient() {
- return recipient;
- }
-
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java
deleted file mode 100644
index acca9f8375..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import android.content.Context;
-import android.content.DialogInterface;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-
-import java.util.concurrent.TimeUnit;
-
-import network.loki.messenger.R;
-
-public class MuteDialog extends AlertDialog {
-
-
- protected MuteDialog(Context context) {
- super(context);
- }
-
- protected MuteDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
- super(context, cancelable, cancelListener);
- }
-
- protected MuteDialog(Context context, int theme) {
- super(context, theme);
- }
-
- public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.MuteDialog_mute_notifications);
- builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, final int which) {
- final long muteUntil;
-
- switch (which) {
- 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;
- default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
- }
-
- listener.onMuted(muteUntil);
- }
- });
-
- builder.show();
-
- }
-
- public interface MuteSelectionListener {
- public void onMuted(long until);
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
new file mode 100644
index 0000000000..f294e387ff
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
@@ -0,0 +1,27 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
+import network.loki.messenger.R
+import java.util.concurrent.TimeUnit
+
+fun showMuteDialog(
+ context: Context,
+ onMuteDuration: (Long) -> Unit
+): AlertDialog = context.showSessionDialog {
+ title(R.string.MuteDialog_mute_notifications)
+ items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
+ onMuteDuration(Option.values()[it].getTime())
+ }
+}
+
+private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
+ ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
+ TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)),
+ ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
+ SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
+ FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
+
+ constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration })
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
index 63b42c4936..afc993df8a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
@@ -210,8 +210,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
try {
signature = biometricSecretProvider.getOrCreateBiometricSignature(this);
hasSignatureObject = true;
- throw new InvalidKeyException("e");
- } catch (InvalidKeyException e) {
+ } catch (Exception e) {
signature = null;
hasSignatureObject = false;
Log.e(TAG, "Error getting / creating signature", e);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
index a791d77a57..bf2aba63f3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
@@ -9,13 +9,14 @@ import android.os.Bundle;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
+import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.onboarding.LandingActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
-import org.session.libsession.utilities.TextSecurePreferences;
import java.util.Locale;
@@ -168,7 +169,13 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
};
IntentFilter filter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
- registerReceiver(clearKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
+ ContextCompat.registerReceiver(
+ this,
+ clearKeyReceiver, filter,
+ KeyCachingService.KEY_PERMISSION,
+ null,
+ ContextCompat.RECEIVER_NOT_EXPORTED
+ );
}
private void removeClearKeyReceiver(Context context) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
new file mode 100644
index 0000000000..598977392b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
@@ -0,0 +1,150 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.LinearLayout.VERTICAL
+import android.widget.Space
+import android.widget.TextView
+import androidx.annotation.AttrRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import androidx.annotation.StyleRes
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.setMargins
+import androidx.core.view.updateMargins
+import androidx.fragment.app.Fragment
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.util.toPx
+
+@DslMarker
+@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
+annotation class DialogDsl
+
+@DialogDsl
+class SessionDialogBuilder(val context: Context) {
+
+ private val dp20 = toPx(20, context.resources)
+ private val dp40 = toPx(40, context.resources)
+ private val dp60 = toPx(60, context.resources)
+
+ private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
+
+ private var dialog: AlertDialog? = null
+ private fun dismiss() = dialog?.dismiss()
+
+ private val topView = LinearLayout(context)
+ .apply { setPadding(0, dp20, 0, 0) }
+ .apply { orientation = VERTICAL }
+ .also(dialogBuilder::setCustomTitle)
+ private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
+ private val buttonLayout = LinearLayout(context)
+
+ private val root = LinearLayout(context).apply { orientation = VERTICAL }
+ .also(dialogBuilder::setView)
+ .apply {
+ addView(contentView)
+ addView(buttonLayout)
+ }
+
+ fun title(@StringRes id: Int) = title(context.getString(id))
+
+ fun title(text: CharSequence?) = title(text?.toString())
+ fun title(text: String?) {
+ text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20, 0, dp20, 0) }
+ }
+
+ fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
+ fun text(text: CharSequence?, @StyleRes style: Int = 0) {
+ text(text, style) {
+ layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ .apply { updateMargins(dp40, 0, dp40, 0) }
+ }
+ }
+
+ private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
+ text ?: return
+ TextView(context, null, 0, style)
+ .apply {
+ setText(text)
+ textAlignment = View.TEXT_ALIGNMENT_CENTER
+ modify()
+ }.let(topView::addView)
+
+ Space(context).apply {
+ layoutParams = LinearLayout.LayoutParams(0, dp20)
+ }.let(topView::addView)
+ }
+
+ fun view(view: View) = contentView.addView(view)
+
+ fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
+
+ fun iconAttribute(@AttrRes icon: Int): AlertDialog.Builder = dialogBuilder.setIconAttribute(icon)
+
+ fun singleChoiceItems(
+ options: Collection,
+ currentSelected: Int = 0,
+ onSelect: (Int) -> Unit
+ ) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect)
+
+ fun singleChoiceItems(
+ options: Array,
+ currentSelected: Int = 0,
+ onSelect: (Int) -> Unit
+ ): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems(
+ options,
+ currentSelected
+ ) { dialog, it -> onSelect(it); dialog.dismiss() }
+
+ fun items(
+ options: Array,
+ onSelect: (Int) -> Unit
+ ): AlertDialog.Builder = dialogBuilder.setItems(
+ options,
+ ) { dialog, it -> onSelect(it); dialog.dismiss() }
+
+ fun destructiveButton(
+ @StringRes text: Int,
+ @StringRes contentDescription: Int = text,
+ listener: () -> Unit = {}
+ ) = button(
+ text,
+ contentDescription,
+ R.style.Widget_Session_Button_Dialog_DestructiveText,
+ ) { listener() }
+
+ fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() }
+ fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button) { listener() }
+
+ fun button(
+ @StringRes text: Int,
+ @StringRes contentDescriptionRes: Int = text,
+ @StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
+ dismiss: Boolean = true,
+ listener: (() -> Unit) = {}
+ ) = Button(context, null, 0, style).apply {
+ setText(text)
+ contentDescription = resources.getString(contentDescriptionRes)
+ layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
+ setOnClickListener {
+ listener.invoke()
+ if (dismiss) dismiss()
+ }
+ }.let(buttonLayout::addView)
+
+ fun create(): AlertDialog = dialogBuilder.create().also { dialog = it }
+ fun show(): AlertDialog = dialogBuilder.show().also { dialog = it }
+}
+
+fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
+ SessionDialogBuilder(this).apply { build() }.show()
+
+fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
+ SessionDialogBuilder(requireContext()).apply { build() }.show()
+fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
+ SessionDialogBuilder(requireContext()).apply { build() }.create()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/Unbindable.java b/app/src/main/java/org/thoughtcrime/securesms/Unbindable.java
deleted file mode 100644
index 3dd5cd8cc0..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/Unbindable.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package org.thoughtcrime.securesms;
-
-public interface Unbindable {
- public void unbind();
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentServer.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentServer.java
index e186007ee3..176a8c290f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentServer.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentServer.java
@@ -50,7 +50,7 @@ public class AttachmentServer implements Runnable {
throws IOException
{
try {
- this.context = context;
+ this.context = context.getApplicationContext();
this.attachment = attachment;
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
this.port = socket.getLocalPort();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
index fa0fce7bd3..6445abed3b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
@@ -5,6 +5,8 @@ import android.text.TextUtils
import com.google.protobuf.ByteString
import org.greenrobot.eventbus.EventBus
import org.session.libsession.database.MessageDataProvider
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
@@ -74,10 +76,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value)
}
- override fun getMessageForQuote(timestamp: Long, author: Address): Pair? {
+ override fun getMessageForQuote(timestamp: Long, author: Address): Triple? {
val messagingDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
val message = messagingDatabase.getMessageFor(timestamp, author)
- return if (message != null) Pair(message.id, message.isMms) else null
+ return if (message != null) Triple(message.id, message.isMms, message.body) else null
}
override fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List {
@@ -176,35 +178,63 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return messageDB.getMessageID(serverId, threadId)
}
+ override fun getMessageIDs(serverIds: List, threadId: Long): Pair, List> {
+ val messageDB = DatabaseComponent.get(context).lokiMessageDatabase()
+ return messageDB.getMessageIDs(serverIds, threadId)
+ }
+
override fun deleteMessage(messageID: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
+ val (threadId, timestamp) = runCatching { messagingDatabase.getMessageRecord(messageID).run { threadId to timestamp } }.getOrNull() ?: (null to null)
+
messagingDatabase.deleteMessage(messageID)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
- DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID)
+ DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
+
+ threadId ?: return
+ timestamp ?: return
+ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(threadId, timestamp)
}
- override fun updateMessageAsDeleted(timestamp: Long, author: String) {
+ override fun deleteMessages(messageIDs: List, threadId: Long, isSms: Boolean) {
+
+ val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
+ else DatabaseComponent.get(context).mmsDatabase()
+
+ val messages = messageIDs.mapNotNull { runCatching { messagingDatabase.getMessageRecord(it) }.getOrNull() }
+
+ // Perform local delete
+ messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
+
+ // Perform online delete
+ DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
+ DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
+
+ val threadId = messages.firstOrNull()?.threadId
+ threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) }
+ }
+
+ override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = Address.fromSerialized(author)
- val message = database.getMessageFor(timestamp, address) ?: return
+ val message = database.getMessageFor(timestamp, address) ?: return null
val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase()
else DatabaseComponent.get(context).smsDatabase()
- messagingDatabase.markAsDeleted(message.id, message.isRead)
+ messagingDatabase.markAsDeleted(message.id, message.isRead, message.hasMention)
if (message.isOutgoing) {
messagingDatabase.deleteMessage(message.id)
}
+
+ return message.id
}
- override fun getServerHashForMessage(messageID: Long): String? {
- val messageDB = DatabaseComponent.get(context).lokiMessageDatabase()
- return messageDB.getMessageServerHash(messageID)
- }
+ override fun getServerHashForMessage(messageID: Long, mms: Boolean): String? =
+ DatabaseComponent.get(context).lokiMessageDatabase().getMessageServerHash(messageID, mms)
- override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
- val attachmentDatabase = DatabaseComponent.get(context).attachmentDatabase()
- return attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0))
- }
+ override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? =
+ DatabaseComponent.get(context).attachmentDatabase()
+ .getAttachment(AttachmentId(attachmentId, 0))
private fun scaleAndStripExif(attachmentDatabase: AttachmentDatabase, constraints: MediaConstraints, attachment: Attachment): Attachment? {
return try {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt
index 94c7517eb0..9c7ca21e8b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt
@@ -7,12 +7,21 @@ import android.os.Build
import android.os.Handler
import android.provider.MediaStore
import androidx.annotation.RequiresApi
+import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer
+
+private const val TAG = "ScreenshotObserver"
class ScreenshotObserver(private val context: Context, handler: Handler, private val screenshotTriggered: ()->Unit): ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
uri ?: return
+
+ // There is an odd bug where we can get notified for changes to 'content://media/external'
+ // directly which is a protected folder, this code is to prevent that crash
+ if (uri.scheme == "content" && uri.host == "media" && uri.path == "/external") { return }
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryRelativeDataColumn(uri)
} else {
@@ -26,22 +35,26 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
val projection = arrayOf(
MediaStore.Images.Media.DATA
)
- context.contentResolver.query(
- uri,
- projection,
- null,
- null,
- null
- )?.use { cursor ->
- val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
- while (cursor.moveToNext()) {
- val path = cursor.getString(dataColumn)
- if (path.contains("screenshot", true)) {
- if (cache.add(uri.hashCode())) {
- screenshotTriggered()
+ try {
+ context.contentResolver.query(
+ uri,
+ projection,
+ null,
+ null,
+ null
+ )?.use { cursor ->
+ val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
+ while (cursor.moveToNext()) {
+ val path = cursor.getString(dataColumn)
+ if (path.contains("screenshot", true)) {
+ if (cache.add(uri.hashCode())) {
+ screenshotTriggered()
+ }
}
}
}
+ } catch (e: SecurityException) {
+ Log.e(TAG, e)
}
}
@@ -51,28 +64,32 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.RELATIVE_PATH
)
- context.contentResolver.query(
- uri,
- projection,
- null,
- null,
- null
- )?.use { cursor ->
- val relativePathColumn =
- cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH)
- val displayNameColumn =
- cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
- while (cursor.moveToNext()) {
- val name = cursor.getString(displayNameColumn)
- val relativePath = cursor.getString(relativePathColumn)
- if (name.contains("screenshot", true) or
- relativePath.contains("screenshot", true)) {
- if (cache.add(uri.hashCode())) {
- screenshotTriggered()
+
+ try {
+ context.contentResolver.query(
+ uri,
+ projection,
+ null,
+ null,
+ null
+ )?.use { cursor ->
+ val relativePathColumn =
+ cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH)
+ val displayNameColumn =
+ cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
+ while (cursor.moveToNext()) {
+ val name = cursor.getString(displayNameColumn)
+ val relativePath = cursor.getString(relativePathColumn)
+ if (name.contains("screenshot", true) or
+ relativePath.contains("screenshot", true)) {
+ if (cache.add(uri.hashCode())) {
+ screenshotTriggered()
+ }
}
}
}
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, e)
}
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java
index 35cbf16b63..fd265337f9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java
@@ -45,7 +45,8 @@ public class AudioRecorder {
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
try {
if (audioCodec != null) {
- throw new AssertionError("We can only record once at a time.");
+ Log.e(TAG, "Trying to start recording while another recording is in progress, exiting...");
+ return;
}
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java
index 61a92105aa..ef404bb070 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java
@@ -80,6 +80,11 @@ public class AudioSlidePlayer implements SensorEventListener {
}
}
+ @Nullable
+ public synchronized static AudioSlidePlayer getInstance() {
+ return playing.orNull();
+ }
+
private AudioSlidePlayer(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java
deleted file mode 100644
index 76342898b1..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package org.thoughtcrime.securesms.backup;
-
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-
-import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
-import org.session.libsignal.utilities.Log;
-import org.thoughtcrime.securesms.util.BackupDirSelector;
-import org.thoughtcrime.securesms.util.BackupUtil;
-
-import org.session.libsession.utilities.Util;
-
-import java.io.IOException;
-
-import network.loki.messenger.R;
-
-public class BackupDialog {
- private static final String TAG = "BackupDialog";
-
- public static void showEnableBackupDialog(
- @NonNull Context context,
- @NonNull SwitchPreferenceCompat preference,
- @NonNull BackupDirSelector backupDirSelector) {
-
- String[] password = BackupUtil.generateBackupPassphrase();
- String passwordSt = Util.join(password, "");
-
- AlertDialog dialog = new AlertDialog.Builder(context)
- .setTitle(R.string.BackupDialog_enable_local_backups)
- .setView(R.layout.backup_enable_dialog)
- .setPositiveButton(R.string.BackupDialog_enable_backups, null)
- .setNegativeButton(android.R.string.cancel, null)
- .create();
-
- dialog.setOnShowListener(created -> {
- Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
- button.setOnClickListener(v -> {
- CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
- if (confirmationCheckBox.isChecked()) {
- backupDirSelector.selectBackupDir(true, uri -> {
- try {
- BackupUtil.enableBackups(context, passwordSt);
- } catch (IOException e) {
- Log.e(TAG, "Failed to activate backups.", e);
- Toast.makeText(context,
- context.getString(R.string.dialog_backup_activation_failed),
- Toast.LENGTH_LONG)
- .show();
- return;
- }
-
- preference.setChecked(true);
- created.dismiss();
- });
- } else {
- Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
- }
- });
- });
-
- dialog.show();
-
- CheckBox checkBox = dialog.findViewById(R.id.confirmation_check);
- TextView textView = dialog.findViewById(R.id.confirmation_text);
-
- ((TextView)dialog.findViewById(R.id.code_first)).setText(password[0]);
- ((TextView)dialog.findViewById(R.id.code_second)).setText(password[1]);
- ((TextView)dialog.findViewById(R.id.code_third)).setText(password[2]);
-
- ((TextView)dialog.findViewById(R.id.code_fourth)).setText(password[3]);
- ((TextView)dialog.findViewById(R.id.code_fifth)).setText(password[4]);
- ((TextView)dialog.findViewById(R.id.code_sixth)).setText(password[5]);
-
- textView.setOnClickListener(v -> checkBox.toggle());
-
- dialog.findViewById(R.id.number_table).setOnClickListener(v -> {
- ((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", passwordSt));
- Toast.makeText(context, R.string.BackupDialog_copied_to_clipboard, Toast.LENGTH_SHORT).show();
- });
-
-
- }
-
- public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
- new AlertDialog.Builder(context)
- .setTitle(R.string.BackupDialog_delete_backups)
- .setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
- BackupUtil.disableBackups(context, true);
- preference.setChecked(false);
- })
- .create()
- .show();
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupEvent.kt
deleted file mode 100644
index 614dc30bba..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupEvent.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.thoughtcrime.securesms.backup
-
-data class BackupEvent constructor(val type: Type, val count: Int, val exception: Exception?) {
-
- enum class Type {
- PROGRESS, FINISHED
- }
-
- companion object {
- @JvmStatic fun createProgress(count: Int) = BackupEvent(Type.PROGRESS, count, null)
- @JvmStatic fun createFinished() = BackupEvent(Type.FINISHED, 0, null)
- @JvmStatic fun createFinished(e: Exception?) = BackupEvent(Type.FINISHED, 0, e)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupPassphrase.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupPassphrase.java
deleted file mode 100644
index eec2a2e588..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupPassphrase.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.thoughtcrime.securesms.backup;
-
-import android.content.Context;
-import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
-import org.session.libsignal.utilities.Log;
-import org.session.libsession.utilities.TextSecurePreferences;
-
-/**
- * Allows the getting and setting of the backup passphrase, which is stored encrypted on API >= 23.
- */
-public class BackupPassphrase {
-
- private static final String TAG = BackupPassphrase.class.getSimpleName();
-
- public static @Nullable String get(@NonNull Context context) {
- String passphrase = TextSecurePreferences.getBackupPassphrase(context);
- String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
-
- if (Build.VERSION.SDK_INT < 23 || (passphrase == null && encryptedPassphrase == null)) {
- return passphrase;
- }
-
- if (encryptedPassphrase == null) {
- Log.i(TAG, "Migrating to encrypted passphrase.");
- set(context, passphrase);
- encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
- }
-
- KeyStoreHelper.SealedData data = KeyStoreHelper.SealedData.fromString(encryptedPassphrase);
- return new String(KeyStoreHelper.unseal(data));
- }
-
- public static void set(@NonNull Context context, @Nullable String passphrase) {
- if (passphrase == null || Build.VERSION.SDK_INT < 23) {
- TextSecurePreferences.setBackupPassphrase(context, passphrase);
- TextSecurePreferences.setEncryptedBackupPassphrase(context, null);
- } else {
- KeyStoreHelper.SealedData encryptedPassphrase = KeyStoreHelper.seal(passphrase.getBytes());
- TextSecurePreferences.setEncryptedBackupPassphrase(context, encryptedPassphrase.serialize());
- TextSecurePreferences.setBackupPassphrase(context, null);
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupPreferences.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupPreferences.kt
deleted file mode 100644
index 8ddfc23a8b..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupPreferences.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-package org.thoughtcrime.securesms.backup
-
-import android.content.Context
-import android.content.SharedPreferences
-import android.os.Build
-import android.preference.PreferenceManager
-import android.preference.PreferenceManager.getDefaultSharedPreferencesName
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.utilities.Log
-import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_BOOLEAN
-import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_INT
-import java.util.*
-
-object BackupPreferences {
- // region Backup related
- fun getBackupRecords(context: Context): List {
- val preferences = PreferenceManager.getDefaultSharedPreferences(context)
- val prefsFileName: String
- prefsFileName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- getDefaultSharedPreferencesName(context)
- } else {
- context.packageName + "_preferences"
- }
- val prefList: LinkedList = LinkedList()
- addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_REGISTRATION_ID_PREF)
- addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_NUMBER_PREF)
- addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_NAME_PREF)
- addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_URL_PREF)
- addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_ID_PREF)
- addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_KEY_PREF)
- addBackupEntryBoolean(prefList, preferences, prefsFileName, TextSecurePreferences.IS_USING_FCM)
- return prefList
- }
-
- private fun addBackupEntryString(
- outPrefList: MutableList,
- prefs: SharedPreferences,
- prefFileName: String,
- prefKey: String,
- ) {
- val value = prefs.getString(prefKey, null)
- if (value == null) {
- logBackupEntry(prefKey, false)
- return
- }
- outPrefList.add(BackupProtos.SharedPreference.newBuilder()
- .setFile(prefFileName)
- .setKey(prefKey)
- .setValue(value)
- .build())
- logBackupEntry(prefKey, true)
- }
-
- private fun addBackupEntryInt(
- outPrefList: MutableList,
- prefs: SharedPreferences,
- prefFileName: String,
- prefKey: String,
- ) {
- val value = prefs.getInt(prefKey, -1)
- if (value == -1) {
- logBackupEntry(prefKey, false)
- return
- }
- outPrefList.add(BackupProtos.SharedPreference.newBuilder()
- .setFile(prefFileName)
- .setKey(PREF_PREFIX_TYPE_INT + prefKey) // The prefix denotes the type of the preference.
- .setValue(value.toString())
- .build())
- logBackupEntry(prefKey, true)
- }
-
- private fun addBackupEntryBoolean(
- outPrefList: MutableList,
- prefs: SharedPreferences,
- prefFileName: String,
- prefKey: String,
- ) {
- if (!prefs.contains(prefKey)) {
- logBackupEntry(prefKey, false)
- return
- }
- outPrefList.add(BackupProtos.SharedPreference.newBuilder()
- .setFile(prefFileName)
- .setKey(PREF_PREFIX_TYPE_BOOLEAN + prefKey) // The prefix denotes the type of the preference.
- .setValue(prefs.getBoolean(prefKey, false).toString())
- .build())
- logBackupEntry(prefKey, true)
- }
-
- private fun logBackupEntry(prefName: String, wasIncluded: Boolean) {
- val sb = StringBuilder()
- sb.append("Backup preference ")
- sb.append(if (wasIncluded) "+ " else "- ")
- sb.append('\"').append(prefName).append("\" ")
- if (!wasIncluded) {
- sb.append("(is empty and not included)")
- }
- Log.d("Loki", sb.toString())
- } // endregion
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupProtos.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupProtos.java
deleted file mode 100644
index f3b78606f4..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupProtos.java
+++ /dev/null
@@ -1,6778 +0,0 @@
-// Generated by the protocol buffer compiler. DO NOT EDIT!
-// source: Backups.proto
-
-package org.thoughtcrime.securesms.backup;
-
-public final class BackupProtos {
- private BackupProtos() {}
- public static void registerAllExtensions(
- com.google.protobuf.ExtensionRegistry registry) {
- }
- public interface SqlStatementOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional string statement = 1;
- /**
- * optional string statement = 1;
- */
- boolean hasStatement();
- /**
- * optional string statement = 1;
- */
- java.lang.String getStatement();
- /**
- * optional string statement = 1;
- */
- com.google.protobuf.ByteString
- getStatementBytes();
-
- // repeated .signal.SqlStatement.SqlParameter parameters = 2;
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- java.util.List
- getParametersList();
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter getParameters(int index);
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- int getParametersCount();
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- java.util.List extends org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder>
- getParametersOrBuilderList();
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder getParametersOrBuilder(
- int index);
- }
- /**
- * Protobuf type {@code signal.SqlStatement}
- */
- public static final class SqlStatement extends
- com.google.protobuf.GeneratedMessage
- implements SqlStatementOrBuilder {
- // Use SqlStatement.newBuilder() to construct.
- private SqlStatement(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private SqlStatement(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final SqlStatement defaultInstance;
- public static SqlStatement getDefaultInstance() {
- return defaultInstance;
- }
-
- public SqlStatement getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private SqlStatement(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 10: {
- bitField0_ |= 0x00000001;
- statement_ = input.readBytes();
- break;
- }
- case 18: {
- if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) {
- parameters_ = new java.util.ArrayList();
- mutable_bitField0_ |= 0x00000002;
- }
- parameters_.add(input.readMessage(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.PARSER, extensionRegistry));
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) {
- parameters_ = java.util.Collections.unmodifiableList(parameters_);
- }
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.class, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public SqlStatement parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new SqlStatement(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- public interface SqlParameterOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional string stringParamter = 1;
- /**
- * optional string stringParamter = 1;
- */
- boolean hasStringParamter();
- /**
- * optional string stringParamter = 1;
- */
- java.lang.String getStringParamter();
- /**
- * optional string stringParamter = 1;
- */
- com.google.protobuf.ByteString
- getStringParamterBytes();
-
- // optional uint64 integerParameter = 2;
- /**
- * optional uint64 integerParameter = 2;
- */
- boolean hasIntegerParameter();
- /**
- * optional uint64 integerParameter = 2;
- */
- long getIntegerParameter();
-
- // optional double doubleParameter = 3;
- /**
- * optional double doubleParameter = 3;
- */
- boolean hasDoubleParameter();
- /**
- * optional double doubleParameter = 3;
- */
- double getDoubleParameter();
-
- // optional bytes blobParameter = 4;
- /**
- * optional bytes blobParameter = 4;
- */
- boolean hasBlobParameter();
- /**
- * optional bytes blobParameter = 4;
- */
- com.google.protobuf.ByteString getBlobParameter();
-
- // optional bool nullparameter = 5;
- /**
- * optional bool nullparameter = 5;
- */
- boolean hasNullparameter();
- /**
- * optional bool nullparameter = 5;
- */
- boolean getNullparameter();
- }
- /**
- * Protobuf type {@code signal.SqlStatement.SqlParameter}
- */
- public static final class SqlParameter extends
- com.google.protobuf.GeneratedMessage
- implements SqlParameterOrBuilder {
- // Use SqlParameter.newBuilder() to construct.
- private SqlParameter(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private SqlParameter(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final SqlParameter defaultInstance;
- public static SqlParameter getDefaultInstance() {
- return defaultInstance;
- }
-
- public SqlParameter getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private SqlParameter(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 10: {
- bitField0_ |= 0x00000001;
- stringParamter_ = input.readBytes();
- break;
- }
- case 16: {
- bitField0_ |= 0x00000002;
- integerParameter_ = input.readUInt64();
- break;
- }
- case 25: {
- bitField0_ |= 0x00000004;
- doubleParameter_ = input.readDouble();
- break;
- }
- case 34: {
- bitField0_ |= 0x00000008;
- blobParameter_ = input.readBytes();
- break;
- }
- case 40: {
- bitField0_ |= 0x00000010;
- nullparameter_ = input.readBool();
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_SqlParameter_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_SqlParameter_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.class, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public SqlParameter parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new SqlParameter(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- private int bitField0_;
- // optional string stringParamter = 1;
- public static final int STRINGPARAMTER_FIELD_NUMBER = 1;
- private java.lang.Object stringParamter_;
- /**
- * optional string stringParamter = 1;
- */
- public boolean hasStringParamter() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional string stringParamter = 1;
- */
- public java.lang.String getStringParamter() {
- java.lang.Object ref = stringParamter_;
- if (ref instanceof java.lang.String) {
- return (java.lang.String) ref;
- } else {
- com.google.protobuf.ByteString bs =
- (com.google.protobuf.ByteString) ref;
- java.lang.String s = bs.toStringUtf8();
- if (bs.isValidUtf8()) {
- stringParamter_ = s;
- }
- return s;
- }
- }
- /**
- * optional string stringParamter = 1;
- */
- public com.google.protobuf.ByteString
- getStringParamterBytes() {
- java.lang.Object ref = stringParamter_;
- if (ref instanceof java.lang.String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- stringParamter_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
-
- // optional uint64 integerParameter = 2;
- public static final int INTEGERPARAMETER_FIELD_NUMBER = 2;
- private long integerParameter_;
- /**
- * optional uint64 integerParameter = 2;
- */
- public boolean hasIntegerParameter() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional uint64 integerParameter = 2;
- */
- public long getIntegerParameter() {
- return integerParameter_;
- }
-
- // optional double doubleParameter = 3;
- public static final int DOUBLEPARAMETER_FIELD_NUMBER = 3;
- private double doubleParameter_;
- /**
- * optional double doubleParameter = 3;
- */
- public boolean hasDoubleParameter() {
- return ((bitField0_ & 0x00000004) == 0x00000004);
- }
- /**
- * optional double doubleParameter = 3;
- */
- public double getDoubleParameter() {
- return doubleParameter_;
- }
-
- // optional bytes blobParameter = 4;
- public static final int BLOBPARAMETER_FIELD_NUMBER = 4;
- private com.google.protobuf.ByteString blobParameter_;
- /**
- * optional bytes blobParameter = 4;
- */
- public boolean hasBlobParameter() {
- return ((bitField0_ & 0x00000008) == 0x00000008);
- }
- /**
- * optional bytes blobParameter = 4;
- */
- public com.google.protobuf.ByteString getBlobParameter() {
- return blobParameter_;
- }
-
- // optional bool nullparameter = 5;
- public static final int NULLPARAMETER_FIELD_NUMBER = 5;
- private boolean nullparameter_;
- /**
- * optional bool nullparameter = 5;
- */
- public boolean hasNullparameter() {
- return ((bitField0_ & 0x00000010) == 0x00000010);
- }
- /**
- * optional bool nullparameter = 5;
- */
- public boolean getNullparameter() {
- return nullparameter_;
- }
-
- private void initFields() {
- stringParamter_ = "";
- integerParameter_ = 0L;
- doubleParameter_ = 0D;
- blobParameter_ = com.google.protobuf.ByteString.EMPTY;
- nullparameter_ = false;
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeBytes(1, getStringParamterBytes());
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- output.writeUInt64(2, integerParameter_);
- }
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- output.writeDouble(3, doubleParameter_);
- }
- if (((bitField0_ & 0x00000008) == 0x00000008)) {
- output.writeBytes(4, blobParameter_);
- }
- if (((bitField0_ & 0x00000010) == 0x00000010)) {
- output.writeBool(5, nullparameter_);
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(1, getStringParamterBytes());
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- size += com.google.protobuf.CodedOutputStream
- .computeUInt64Size(2, integerParameter_);
- }
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- size += com.google.protobuf.CodedOutputStream
- .computeDoubleSize(3, doubleParameter_);
- }
- if (((bitField0_ & 0x00000008) == 0x00000008)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(4, blobParameter_);
- }
- if (((bitField0_ & 0x00000010) == 0x00000010)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBoolSize(5, nullparameter_);
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.SqlStatement.SqlParameter}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_SqlParameter_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_SqlParameter_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.class, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- stringParamter_ = "";
- bitField0_ = (bitField0_ & ~0x00000001);
- integerParameter_ = 0L;
- bitField0_ = (bitField0_ & ~0x00000002);
- doubleParameter_ = 0D;
- bitField0_ = (bitField0_ & ~0x00000004);
- blobParameter_ = com.google.protobuf.ByteString.EMPTY;
- bitField0_ = (bitField0_ & ~0x00000008);
- nullparameter_ = false;
- bitField0_ = (bitField0_ & ~0x00000010);
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_SqlParameter_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter build() {
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter result = new org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- result.stringParamter_ = stringParamter_;
- if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
- to_bitField0_ |= 0x00000002;
- }
- result.integerParameter_ = integerParameter_;
- if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
- to_bitField0_ |= 0x00000004;
- }
- result.doubleParameter_ = doubleParameter_;
- if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
- to_bitField0_ |= 0x00000008;
- }
- result.blobParameter_ = blobParameter_;
- if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
- to_bitField0_ |= 0x00000010;
- }
- result.nullparameter_ = nullparameter_;
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.getDefaultInstance()) return this;
- if (other.hasStringParamter()) {
- bitField0_ |= 0x00000001;
- stringParamter_ = other.stringParamter_;
- onChanged();
- }
- if (other.hasIntegerParameter()) {
- setIntegerParameter(other.getIntegerParameter());
- }
- if (other.hasDoubleParameter()) {
- setDoubleParameter(other.getDoubleParameter());
- }
- if (other.hasBlobParameter()) {
- setBlobParameter(other.getBlobParameter());
- }
- if (other.hasNullparameter()) {
- setNullparameter(other.getNullparameter());
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional string stringParamter = 1;
- private java.lang.Object stringParamter_ = "";
- /**
- * optional string stringParamter = 1;
- */
- public boolean hasStringParamter() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional string stringParamter = 1;
- */
- public java.lang.String getStringParamter() {
- java.lang.Object ref = stringParamter_;
- if (!(ref instanceof java.lang.String)) {
- java.lang.String s = ((com.google.protobuf.ByteString) ref)
- .toStringUtf8();
- stringParamter_ = s;
- return s;
- } else {
- return (java.lang.String) ref;
- }
- }
- /**
- * optional string stringParamter = 1;
- */
- public com.google.protobuf.ByteString
- getStringParamterBytes() {
- java.lang.Object ref = stringParamter_;
- if (ref instanceof String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- stringParamter_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
- /**
- * optional string stringParamter = 1;
- */
- public Builder setStringParamter(
- java.lang.String value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- stringParamter_ = value;
- onChanged();
- return this;
- }
- /**
- * optional string stringParamter = 1;
- */
- public Builder clearStringParamter() {
- bitField0_ = (bitField0_ & ~0x00000001);
- stringParamter_ = getDefaultInstance().getStringParamter();
- onChanged();
- return this;
- }
- /**
- * optional string stringParamter = 1;
- */
- public Builder setStringParamterBytes(
- com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- stringParamter_ = value;
- onChanged();
- return this;
- }
-
- // optional uint64 integerParameter = 2;
- private long integerParameter_ ;
- /**
- * optional uint64 integerParameter = 2;
- */
- public boolean hasIntegerParameter() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional uint64 integerParameter = 2;
- */
- public long getIntegerParameter() {
- return integerParameter_;
- }
- /**
- * optional uint64 integerParameter = 2;
- */
- public Builder setIntegerParameter(long value) {
- bitField0_ |= 0x00000002;
- integerParameter_ = value;
- onChanged();
- return this;
- }
- /**
- * optional uint64 integerParameter = 2;
- */
- public Builder clearIntegerParameter() {
- bitField0_ = (bitField0_ & ~0x00000002);
- integerParameter_ = 0L;
- onChanged();
- return this;
- }
-
- // optional double doubleParameter = 3;
- private double doubleParameter_ ;
- /**
- * optional double doubleParameter = 3;
- */
- public boolean hasDoubleParameter() {
- return ((bitField0_ & 0x00000004) == 0x00000004);
- }
- /**
- * optional double doubleParameter = 3;
- */
- public double getDoubleParameter() {
- return doubleParameter_;
- }
- /**
- * optional double doubleParameter = 3;
- */
- public Builder setDoubleParameter(double value) {
- bitField0_ |= 0x00000004;
- doubleParameter_ = value;
- onChanged();
- return this;
- }
- /**
- * optional double doubleParameter = 3;
- */
- public Builder clearDoubleParameter() {
- bitField0_ = (bitField0_ & ~0x00000004);
- doubleParameter_ = 0D;
- onChanged();
- return this;
- }
-
- // optional bytes blobParameter = 4;
- private com.google.protobuf.ByteString blobParameter_ = com.google.protobuf.ByteString.EMPTY;
- /**
- * optional bytes blobParameter = 4;
- */
- public boolean hasBlobParameter() {
- return ((bitField0_ & 0x00000008) == 0x00000008);
- }
- /**
- * optional bytes blobParameter = 4;
- */
- public com.google.protobuf.ByteString getBlobParameter() {
- return blobParameter_;
- }
- /**
- * optional bytes blobParameter = 4;
- */
- public Builder setBlobParameter(com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000008;
- blobParameter_ = value;
- onChanged();
- return this;
- }
- /**
- * optional bytes blobParameter = 4;
- */
- public Builder clearBlobParameter() {
- bitField0_ = (bitField0_ & ~0x00000008);
- blobParameter_ = getDefaultInstance().getBlobParameter();
- onChanged();
- return this;
- }
-
- // optional bool nullparameter = 5;
- private boolean nullparameter_ ;
- /**
- * optional bool nullparameter = 5;
- */
- public boolean hasNullparameter() {
- return ((bitField0_ & 0x00000010) == 0x00000010);
- }
- /**
- * optional bool nullparameter = 5;
- */
- public boolean getNullparameter() {
- return nullparameter_;
- }
- /**
- * optional bool nullparameter = 5;
- */
- public Builder setNullparameter(boolean value) {
- bitField0_ |= 0x00000010;
- nullparameter_ = value;
- onChanged();
- return this;
- }
- /**
- * optional bool nullparameter = 5;
- */
- public Builder clearNullparameter() {
- bitField0_ = (bitField0_ & ~0x00000010);
- nullparameter_ = false;
- onChanged();
- return this;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.SqlStatement.SqlParameter)
- }
-
- static {
- defaultInstance = new SqlParameter(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.SqlStatement.SqlParameter)
- }
-
- private int bitField0_;
- // optional string statement = 1;
- public static final int STATEMENT_FIELD_NUMBER = 1;
- private java.lang.Object statement_;
- /**
- * optional string statement = 1;
- */
- public boolean hasStatement() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional string statement = 1;
- */
- public java.lang.String getStatement() {
- java.lang.Object ref = statement_;
- if (ref instanceof java.lang.String) {
- return (java.lang.String) ref;
- } else {
- com.google.protobuf.ByteString bs =
- (com.google.protobuf.ByteString) ref;
- java.lang.String s = bs.toStringUtf8();
- if (bs.isValidUtf8()) {
- statement_ = s;
- }
- return s;
- }
- }
- /**
- * optional string statement = 1;
- */
- public com.google.protobuf.ByteString
- getStatementBytes() {
- java.lang.Object ref = statement_;
- if (ref instanceof java.lang.String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- statement_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
-
- // repeated .signal.SqlStatement.SqlParameter parameters = 2;
- public static final int PARAMETERS_FIELD_NUMBER = 2;
- private java.util.List parameters_;
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public java.util.List getParametersList() {
- return parameters_;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public java.util.List extends org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder>
- getParametersOrBuilderList() {
- return parameters_;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public int getParametersCount() {
- return parameters_.size();
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter getParameters(int index) {
- return parameters_.get(index);
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder getParametersOrBuilder(
- int index) {
- return parameters_.get(index);
- }
-
- private void initFields() {
- statement_ = "";
- parameters_ = java.util.Collections.emptyList();
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeBytes(1, getStatementBytes());
- }
- for (int i = 0; i < parameters_.size(); i++) {
- output.writeMessage(2, parameters_.get(i));
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(1, getStatementBytes());
- }
- for (int i = 0; i < parameters_.size(); i++) {
- size += com.google.protobuf.CodedOutputStream
- .computeMessageSize(2, parameters_.get(i));
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.SqlStatement}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.SqlStatementOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.class, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- getParametersFieldBuilder();
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- statement_ = "";
- bitField0_ = (bitField0_ & ~0x00000001);
- if (parametersBuilder_ == null) {
- parameters_ = java.util.Collections.emptyList();
- bitField0_ = (bitField0_ & ~0x00000002);
- } else {
- parametersBuilder_.clear();
- }
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SqlStatement_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement build() {
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement result = new org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- result.statement_ = statement_;
- if (parametersBuilder_ == null) {
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- parameters_ = java.util.Collections.unmodifiableList(parameters_);
- bitField0_ = (bitField0_ & ~0x00000002);
- }
- result.parameters_ = parameters_;
- } else {
- result.parameters_ = parametersBuilder_.build();
- }
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.getDefaultInstance()) return this;
- if (other.hasStatement()) {
- bitField0_ |= 0x00000001;
- statement_ = other.statement_;
- onChanged();
- }
- if (parametersBuilder_ == null) {
- if (!other.parameters_.isEmpty()) {
- if (parameters_.isEmpty()) {
- parameters_ = other.parameters_;
- bitField0_ = (bitField0_ & ~0x00000002);
- } else {
- ensureParametersIsMutable();
- parameters_.addAll(other.parameters_);
- }
- onChanged();
- }
- } else {
- if (!other.parameters_.isEmpty()) {
- if (parametersBuilder_.isEmpty()) {
- parametersBuilder_.dispose();
- parametersBuilder_ = null;
- parameters_ = other.parameters_;
- bitField0_ = (bitField0_ & ~0x00000002);
- parametersBuilder_ =
- com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
- getParametersFieldBuilder() : null;
- } else {
- parametersBuilder_.addAllMessages(other.parameters_);
- }
- }
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional string statement = 1;
- private java.lang.Object statement_ = "";
- /**
- * optional string statement = 1;
- */
- public boolean hasStatement() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional string statement = 1;
- */
- public java.lang.String getStatement() {
- java.lang.Object ref = statement_;
- if (!(ref instanceof java.lang.String)) {
- java.lang.String s = ((com.google.protobuf.ByteString) ref)
- .toStringUtf8();
- statement_ = s;
- return s;
- } else {
- return (java.lang.String) ref;
- }
- }
- /**
- * optional string statement = 1;
- */
- public com.google.protobuf.ByteString
- getStatementBytes() {
- java.lang.Object ref = statement_;
- if (ref instanceof String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- statement_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
- /**
- * optional string statement = 1;
- */
- public Builder setStatement(
- java.lang.String value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- statement_ = value;
- onChanged();
- return this;
- }
- /**
- * optional string statement = 1;
- */
- public Builder clearStatement() {
- bitField0_ = (bitField0_ & ~0x00000001);
- statement_ = getDefaultInstance().getStatement();
- onChanged();
- return this;
- }
- /**
- * optional string statement = 1;
- */
- public Builder setStatementBytes(
- com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- statement_ = value;
- onChanged();
- return this;
- }
-
- // repeated .signal.SqlStatement.SqlParameter parameters = 2;
- private java.util.List parameters_ =
- java.util.Collections.emptyList();
- private void ensureParametersIsMutable() {
- if (!((bitField0_ & 0x00000002) == 0x00000002)) {
- parameters_ = new java.util.ArrayList(parameters_);
- bitField0_ |= 0x00000002;
- }
- }
-
- private com.google.protobuf.RepeatedFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder> parametersBuilder_;
-
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public java.util.List getParametersList() {
- if (parametersBuilder_ == null) {
- return java.util.Collections.unmodifiableList(parameters_);
- } else {
- return parametersBuilder_.getMessageList();
- }
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public int getParametersCount() {
- if (parametersBuilder_ == null) {
- return parameters_.size();
- } else {
- return parametersBuilder_.getCount();
- }
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter getParameters(int index) {
- if (parametersBuilder_ == null) {
- return parameters_.get(index);
- } else {
- return parametersBuilder_.getMessage(index);
- }
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder setParameters(
- int index, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter value) {
- if (parametersBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- ensureParametersIsMutable();
- parameters_.set(index, value);
- onChanged();
- } else {
- parametersBuilder_.setMessage(index, value);
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder setParameters(
- int index, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder builderForValue) {
- if (parametersBuilder_ == null) {
- ensureParametersIsMutable();
- parameters_.set(index, builderForValue.build());
- onChanged();
- } else {
- parametersBuilder_.setMessage(index, builderForValue.build());
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder addParameters(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter value) {
- if (parametersBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- ensureParametersIsMutable();
- parameters_.add(value);
- onChanged();
- } else {
- parametersBuilder_.addMessage(value);
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder addParameters(
- int index, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter value) {
- if (parametersBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- ensureParametersIsMutable();
- parameters_.add(index, value);
- onChanged();
- } else {
- parametersBuilder_.addMessage(index, value);
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder addParameters(
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder builderForValue) {
- if (parametersBuilder_ == null) {
- ensureParametersIsMutable();
- parameters_.add(builderForValue.build());
- onChanged();
- } else {
- parametersBuilder_.addMessage(builderForValue.build());
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder addParameters(
- int index, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder builderForValue) {
- if (parametersBuilder_ == null) {
- ensureParametersIsMutable();
- parameters_.add(index, builderForValue.build());
- onChanged();
- } else {
- parametersBuilder_.addMessage(index, builderForValue.build());
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder addAllParameters(
- java.lang.Iterable extends org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter> values) {
- if (parametersBuilder_ == null) {
- ensureParametersIsMutable();
- super.addAll(values, parameters_);
- onChanged();
- } else {
- parametersBuilder_.addAllMessages(values);
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder clearParameters() {
- if (parametersBuilder_ == null) {
- parameters_ = java.util.Collections.emptyList();
- bitField0_ = (bitField0_ & ~0x00000002);
- onChanged();
- } else {
- parametersBuilder_.clear();
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public Builder removeParameters(int index) {
- if (parametersBuilder_ == null) {
- ensureParametersIsMutable();
- parameters_.remove(index);
- onChanged();
- } else {
- parametersBuilder_.remove(index);
- }
- return this;
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder getParametersBuilder(
- int index) {
- return getParametersFieldBuilder().getBuilder(index);
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder getParametersOrBuilder(
- int index) {
- if (parametersBuilder_ == null) {
- return parameters_.get(index); } else {
- return parametersBuilder_.getMessageOrBuilder(index);
- }
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public java.util.List extends org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder>
- getParametersOrBuilderList() {
- if (parametersBuilder_ != null) {
- return parametersBuilder_.getMessageOrBuilderList();
- } else {
- return java.util.Collections.unmodifiableList(parameters_);
- }
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder addParametersBuilder() {
- return getParametersFieldBuilder().addBuilder(
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.getDefaultInstance());
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder addParametersBuilder(
- int index) {
- return getParametersFieldBuilder().addBuilder(
- index, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.getDefaultInstance());
- }
- /**
- * repeated .signal.SqlStatement.SqlParameter parameters = 2;
- */
- public java.util.List
- getParametersBuilderList() {
- return getParametersFieldBuilder().getBuilderList();
- }
- private com.google.protobuf.RepeatedFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder>
- getParametersFieldBuilder() {
- if (parametersBuilder_ == null) {
- parametersBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameter.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.SqlParameterOrBuilder>(
- parameters_,
- ((bitField0_ & 0x00000002) == 0x00000002),
- getParentForChildren(),
- isClean());
- parameters_ = null;
- }
- return parametersBuilder_;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.SqlStatement)
- }
-
- static {
- defaultInstance = new SqlStatement(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.SqlStatement)
- }
-
- public interface SharedPreferenceOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional string file = 1;
- /**
- * optional string file = 1;
- */
- boolean hasFile();
- /**
- * optional string file = 1;
- */
- java.lang.String getFile();
- /**
- * optional string file = 1;
- */
- com.google.protobuf.ByteString
- getFileBytes();
-
- // optional string key = 2;
- /**
- * optional string key = 2;
- */
- boolean hasKey();
- /**
- * optional string key = 2;
- */
- java.lang.String getKey();
- /**
- * optional string key = 2;
- */
- com.google.protobuf.ByteString
- getKeyBytes();
-
- // optional string value = 3;
- /**
- * optional string value = 3;
- */
- boolean hasValue();
- /**
- * optional string value = 3;
- */
- java.lang.String getValue();
- /**
- * optional string value = 3;
- */
- com.google.protobuf.ByteString
- getValueBytes();
- }
- /**
- * Protobuf type {@code signal.SharedPreference}
- */
- public static final class SharedPreference extends
- com.google.protobuf.GeneratedMessage
- implements SharedPreferenceOrBuilder {
- // Use SharedPreference.newBuilder() to construct.
- private SharedPreference(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private SharedPreference(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final SharedPreference defaultInstance;
- public static SharedPreference getDefaultInstance() {
- return defaultInstance;
- }
-
- public SharedPreference getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private SharedPreference(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 10: {
- bitField0_ |= 0x00000001;
- file_ = input.readBytes();
- break;
- }
- case 18: {
- bitField0_ |= 0x00000002;
- key_ = input.readBytes();
- break;
- }
- case 26: {
- bitField0_ |= 0x00000004;
- value_ = input.readBytes();
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SharedPreference_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SharedPreference_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.class, org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public SharedPreference parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new SharedPreference(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- private int bitField0_;
- // optional string file = 1;
- public static final int FILE_FIELD_NUMBER = 1;
- private java.lang.Object file_;
- /**
- * optional string file = 1;
- */
- public boolean hasFile() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional string file = 1;
- */
- public java.lang.String getFile() {
- java.lang.Object ref = file_;
- if (ref instanceof java.lang.String) {
- return (java.lang.String) ref;
- } else {
- com.google.protobuf.ByteString bs =
- (com.google.protobuf.ByteString) ref;
- java.lang.String s = bs.toStringUtf8();
- if (bs.isValidUtf8()) {
- file_ = s;
- }
- return s;
- }
- }
- /**
- * optional string file = 1;
- */
- public com.google.protobuf.ByteString
- getFileBytes() {
- java.lang.Object ref = file_;
- if (ref instanceof java.lang.String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- file_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
-
- // optional string key = 2;
- public static final int KEY_FIELD_NUMBER = 2;
- private java.lang.Object key_;
- /**
- * optional string key = 2;
- */
- public boolean hasKey() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional string key = 2;
- */
- public java.lang.String getKey() {
- java.lang.Object ref = key_;
- if (ref instanceof java.lang.String) {
- return (java.lang.String) ref;
- } else {
- com.google.protobuf.ByteString bs =
- (com.google.protobuf.ByteString) ref;
- java.lang.String s = bs.toStringUtf8();
- if (bs.isValidUtf8()) {
- key_ = s;
- }
- return s;
- }
- }
- /**
- * optional string key = 2;
- */
- public com.google.protobuf.ByteString
- getKeyBytes() {
- java.lang.Object ref = key_;
- if (ref instanceof java.lang.String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- key_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
-
- // optional string value = 3;
- public static final int VALUE_FIELD_NUMBER = 3;
- private java.lang.Object value_;
- /**
- * optional string value = 3;
- */
- public boolean hasValue() {
- return ((bitField0_ & 0x00000004) == 0x00000004);
- }
- /**
- * optional string value = 3;
- */
- public java.lang.String getValue() {
- java.lang.Object ref = value_;
- if (ref instanceof java.lang.String) {
- return (java.lang.String) ref;
- } else {
- com.google.protobuf.ByteString bs =
- (com.google.protobuf.ByteString) ref;
- java.lang.String s = bs.toStringUtf8();
- if (bs.isValidUtf8()) {
- value_ = s;
- }
- return s;
- }
- }
- /**
- * optional string value = 3;
- */
- public com.google.protobuf.ByteString
- getValueBytes() {
- java.lang.Object ref = value_;
- if (ref instanceof java.lang.String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- value_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
-
- private void initFields() {
- file_ = "";
- key_ = "";
- value_ = "";
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeBytes(1, getFileBytes());
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- output.writeBytes(2, getKeyBytes());
- }
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- output.writeBytes(3, getValueBytes());
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(1, getFileBytes());
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(2, getKeyBytes());
- }
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(3, getValueBytes());
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.SharedPreference}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.SharedPreferenceOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SharedPreference_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SharedPreference_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.class, org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- file_ = "";
- bitField0_ = (bitField0_ & ~0x00000001);
- key_ = "";
- bitField0_ = (bitField0_ & ~0x00000002);
- value_ = "";
- bitField0_ = (bitField0_ & ~0x00000004);
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_SharedPreference_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference build() {
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference result = new org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- result.file_ = file_;
- if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
- to_bitField0_ |= 0x00000002;
- }
- result.key_ = key_;
- if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
- to_bitField0_ |= 0x00000004;
- }
- result.value_ = value_;
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.getDefaultInstance()) return this;
- if (other.hasFile()) {
- bitField0_ |= 0x00000001;
- file_ = other.file_;
- onChanged();
- }
- if (other.hasKey()) {
- bitField0_ |= 0x00000002;
- key_ = other.key_;
- onChanged();
- }
- if (other.hasValue()) {
- bitField0_ |= 0x00000004;
- value_ = other.value_;
- onChanged();
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional string file = 1;
- private java.lang.Object file_ = "";
- /**
- * optional string file = 1;
- */
- public boolean hasFile() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional string file = 1;
- */
- public java.lang.String getFile() {
- java.lang.Object ref = file_;
- if (!(ref instanceof java.lang.String)) {
- java.lang.String s = ((com.google.protobuf.ByteString) ref)
- .toStringUtf8();
- file_ = s;
- return s;
- } else {
- return (java.lang.String) ref;
- }
- }
- /**
- * optional string file = 1;
- */
- public com.google.protobuf.ByteString
- getFileBytes() {
- java.lang.Object ref = file_;
- if (ref instanceof String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- file_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
- /**
- * optional string file = 1;
- */
- public Builder setFile(
- java.lang.String value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- file_ = value;
- onChanged();
- return this;
- }
- /**
- * optional string file = 1;
- */
- public Builder clearFile() {
- bitField0_ = (bitField0_ & ~0x00000001);
- file_ = getDefaultInstance().getFile();
- onChanged();
- return this;
- }
- /**
- * optional string file = 1;
- */
- public Builder setFileBytes(
- com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- file_ = value;
- onChanged();
- return this;
- }
-
- // optional string key = 2;
- private java.lang.Object key_ = "";
- /**
- * optional string key = 2;
- */
- public boolean hasKey() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional string key = 2;
- */
- public java.lang.String getKey() {
- java.lang.Object ref = key_;
- if (!(ref instanceof java.lang.String)) {
- java.lang.String s = ((com.google.protobuf.ByteString) ref)
- .toStringUtf8();
- key_ = s;
- return s;
- } else {
- return (java.lang.String) ref;
- }
- }
- /**
- * optional string key = 2;
- */
- public com.google.protobuf.ByteString
- getKeyBytes() {
- java.lang.Object ref = key_;
- if (ref instanceof String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- key_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
- /**
- * optional string key = 2;
- */
- public Builder setKey(
- java.lang.String value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000002;
- key_ = value;
- onChanged();
- return this;
- }
- /**
- * optional string key = 2;
- */
- public Builder clearKey() {
- bitField0_ = (bitField0_ & ~0x00000002);
- key_ = getDefaultInstance().getKey();
- onChanged();
- return this;
- }
- /**
- * optional string key = 2;
- */
- public Builder setKeyBytes(
- com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000002;
- key_ = value;
- onChanged();
- return this;
- }
-
- // optional string value = 3;
- private java.lang.Object value_ = "";
- /**
- * optional string value = 3;
- */
- public boolean hasValue() {
- return ((bitField0_ & 0x00000004) == 0x00000004);
- }
- /**
- * optional string value = 3;
- */
- public java.lang.String getValue() {
- java.lang.Object ref = value_;
- if (!(ref instanceof java.lang.String)) {
- java.lang.String s = ((com.google.protobuf.ByteString) ref)
- .toStringUtf8();
- value_ = s;
- return s;
- } else {
- return (java.lang.String) ref;
- }
- }
- /**
- * optional string value = 3;
- */
- public com.google.protobuf.ByteString
- getValueBytes() {
- java.lang.Object ref = value_;
- if (ref instanceof String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- value_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
- /**
- * optional string value = 3;
- */
- public Builder setValue(
- java.lang.String value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000004;
- value_ = value;
- onChanged();
- return this;
- }
- /**
- * optional string value = 3;
- */
- public Builder clearValue() {
- bitField0_ = (bitField0_ & ~0x00000004);
- value_ = getDefaultInstance().getValue();
- onChanged();
- return this;
- }
- /**
- * optional string value = 3;
- */
- public Builder setValueBytes(
- com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000004;
- value_ = value;
- onChanged();
- return this;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.SharedPreference)
- }
-
- static {
- defaultInstance = new SharedPreference(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.SharedPreference)
- }
-
- public interface AttachmentOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional uint64 rowId = 1;
- /**
- * optional uint64 rowId = 1;
- */
- boolean hasRowId();
- /**
- * optional uint64 rowId = 1;
- */
- long getRowId();
-
- // optional uint64 attachmentId = 2;
- /**
- * optional uint64 attachmentId = 2;
- */
- boolean hasAttachmentId();
- /**
- * optional uint64 attachmentId = 2;
- */
- long getAttachmentId();
-
- // optional uint32 length = 3;
- /**
- * optional uint32 length = 3;
- */
- boolean hasLength();
- /**
- * optional uint32 length = 3;
- */
- int getLength();
- }
- /**
- * Protobuf type {@code signal.Attachment}
- */
- public static final class Attachment extends
- com.google.protobuf.GeneratedMessage
- implements AttachmentOrBuilder {
- // Use Attachment.newBuilder() to construct.
- private Attachment(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private Attachment(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final Attachment defaultInstance;
- public static Attachment getDefaultInstance() {
- return defaultInstance;
- }
-
- public Attachment getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private Attachment(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 8: {
- bitField0_ |= 0x00000001;
- rowId_ = input.readUInt64();
- break;
- }
- case 16: {
- bitField0_ |= 0x00000002;
- attachmentId_ = input.readUInt64();
- break;
- }
- case 24: {
- bitField0_ |= 0x00000004;
- length_ = input.readUInt32();
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Attachment_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Attachment_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment.class, org.thoughtcrime.securesms.backup.BackupProtos.Attachment.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public Attachment parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new Attachment(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- private int bitField0_;
- // optional uint64 rowId = 1;
- public static final int ROWID_FIELD_NUMBER = 1;
- private long rowId_;
- /**
- * optional uint64 rowId = 1;
- */
- public boolean hasRowId() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional uint64 rowId = 1;
- */
- public long getRowId() {
- return rowId_;
- }
-
- // optional uint64 attachmentId = 2;
- public static final int ATTACHMENTID_FIELD_NUMBER = 2;
- private long attachmentId_;
- /**
- * optional uint64 attachmentId = 2;
- */
- public boolean hasAttachmentId() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional uint64 attachmentId = 2;
- */
- public long getAttachmentId() {
- return attachmentId_;
- }
-
- // optional uint32 length = 3;
- public static final int LENGTH_FIELD_NUMBER = 3;
- private int length_;
- /**
- * optional uint32 length = 3;
- */
- public boolean hasLength() {
- return ((bitField0_ & 0x00000004) == 0x00000004);
- }
- /**
- * optional uint32 length = 3;
- */
- public int getLength() {
- return length_;
- }
-
- private void initFields() {
- rowId_ = 0L;
- attachmentId_ = 0L;
- length_ = 0;
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeUInt64(1, rowId_);
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- output.writeUInt64(2, attachmentId_);
- }
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- output.writeUInt32(3, length_);
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeUInt64Size(1, rowId_);
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- size += com.google.protobuf.CodedOutputStream
- .computeUInt64Size(2, attachmentId_);
- }
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- size += com.google.protobuf.CodedOutputStream
- .computeUInt32Size(3, length_);
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Attachment parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.Attachment prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.Attachment}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.AttachmentOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Attachment_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Attachment_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment.class, org.thoughtcrime.securesms.backup.BackupProtos.Attachment.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.Attachment.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- rowId_ = 0L;
- bitField0_ = (bitField0_ & ~0x00000001);
- attachmentId_ = 0L;
- bitField0_ = (bitField0_ & ~0x00000002);
- length_ = 0;
- bitField0_ = (bitField0_ & ~0x00000004);
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Attachment_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Attachment getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.Attachment.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Attachment build() {
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Attachment buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment result = new org.thoughtcrime.securesms.backup.BackupProtos.Attachment(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- result.rowId_ = rowId_;
- if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
- to_bitField0_ |= 0x00000002;
- }
- result.attachmentId_ = attachmentId_;
- if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
- to_bitField0_ |= 0x00000004;
- }
- result.length_ = length_;
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.Attachment) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.Attachment)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.Attachment other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.Attachment.getDefaultInstance()) return this;
- if (other.hasRowId()) {
- setRowId(other.getRowId());
- }
- if (other.hasAttachmentId()) {
- setAttachmentId(other.getAttachmentId());
- }
- if (other.hasLength()) {
- setLength(other.getLength());
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.Attachment) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional uint64 rowId = 1;
- private long rowId_ ;
- /**
- * optional uint64 rowId = 1;
- */
- public boolean hasRowId() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional uint64 rowId = 1;
- */
- public long getRowId() {
- return rowId_;
- }
- /**
- * optional uint64 rowId = 1;
- */
- public Builder setRowId(long value) {
- bitField0_ |= 0x00000001;
- rowId_ = value;
- onChanged();
- return this;
- }
- /**
- * optional uint64 rowId = 1;
- */
- public Builder clearRowId() {
- bitField0_ = (bitField0_ & ~0x00000001);
- rowId_ = 0L;
- onChanged();
- return this;
- }
-
- // optional uint64 attachmentId = 2;
- private long attachmentId_ ;
- /**
- * optional uint64 attachmentId = 2;
- */
- public boolean hasAttachmentId() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional uint64 attachmentId = 2;
- */
- public long getAttachmentId() {
- return attachmentId_;
- }
- /**
- * optional uint64 attachmentId = 2;
- */
- public Builder setAttachmentId(long value) {
- bitField0_ |= 0x00000002;
- attachmentId_ = value;
- onChanged();
- return this;
- }
- /**
- * optional uint64 attachmentId = 2;
- */
- public Builder clearAttachmentId() {
- bitField0_ = (bitField0_ & ~0x00000002);
- attachmentId_ = 0L;
- onChanged();
- return this;
- }
-
- // optional uint32 length = 3;
- private int length_ ;
- /**
- * optional uint32 length = 3;
- */
- public boolean hasLength() {
- return ((bitField0_ & 0x00000004) == 0x00000004);
- }
- /**
- * optional uint32 length = 3;
- */
- public int getLength() {
- return length_;
- }
- /**
- * optional uint32 length = 3;
- */
- public Builder setLength(int value) {
- bitField0_ |= 0x00000004;
- length_ = value;
- onChanged();
- return this;
- }
- /**
- * optional uint32 length = 3;
- */
- public Builder clearLength() {
- bitField0_ = (bitField0_ & ~0x00000004);
- length_ = 0;
- onChanged();
- return this;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.Attachment)
- }
-
- static {
- defaultInstance = new Attachment(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.Attachment)
- }
-
- public interface StickerOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional uint64 rowId = 1;
- /**
- * optional uint64 rowId = 1;
- */
- boolean hasRowId();
- /**
- * optional uint64 rowId = 1;
- */
- long getRowId();
-
- // optional uint32 length = 2;
- /**
- * optional uint32 length = 2;
- */
- boolean hasLength();
- /**
- * optional uint32 length = 2;
- */
- int getLength();
- }
- /**
- * Protobuf type {@code signal.Sticker}
- */
- public static final class Sticker extends
- com.google.protobuf.GeneratedMessage
- implements StickerOrBuilder {
- // Use Sticker.newBuilder() to construct.
- private Sticker(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private Sticker(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final Sticker defaultInstance;
- public static Sticker getDefaultInstance() {
- return defaultInstance;
- }
-
- public Sticker getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private Sticker(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 8: {
- bitField0_ |= 0x00000001;
- rowId_ = input.readUInt64();
- break;
- }
- case 16: {
- bitField0_ |= 0x00000002;
- length_ = input.readUInt32();
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Sticker_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Sticker_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker.class, org.thoughtcrime.securesms.backup.BackupProtos.Sticker.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public Sticker parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new Sticker(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- private int bitField0_;
- // optional uint64 rowId = 1;
- public static final int ROWID_FIELD_NUMBER = 1;
- private long rowId_;
- /**
- * optional uint64 rowId = 1;
- */
- public boolean hasRowId() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional uint64 rowId = 1;
- */
- public long getRowId() {
- return rowId_;
- }
-
- // optional uint32 length = 2;
- public static final int LENGTH_FIELD_NUMBER = 2;
- private int length_;
- /**
- * optional uint32 length = 2;
- */
- public boolean hasLength() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional uint32 length = 2;
- */
- public int getLength() {
- return length_;
- }
-
- private void initFields() {
- rowId_ = 0L;
- length_ = 0;
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeUInt64(1, rowId_);
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- output.writeUInt32(2, length_);
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeUInt64Size(1, rowId_);
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- size += com.google.protobuf.CodedOutputStream
- .computeUInt32Size(2, length_);
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Sticker parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.Sticker prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.Sticker}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.StickerOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Sticker_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Sticker_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker.class, org.thoughtcrime.securesms.backup.BackupProtos.Sticker.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.Sticker.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- rowId_ = 0L;
- bitField0_ = (bitField0_ & ~0x00000001);
- length_ = 0;
- bitField0_ = (bitField0_ & ~0x00000002);
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Sticker_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Sticker getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.Sticker.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Sticker build() {
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Sticker buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker result = new org.thoughtcrime.securesms.backup.BackupProtos.Sticker(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- result.rowId_ = rowId_;
- if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
- to_bitField0_ |= 0x00000002;
- }
- result.length_ = length_;
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.Sticker) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.Sticker)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.Sticker other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.Sticker.getDefaultInstance()) return this;
- if (other.hasRowId()) {
- setRowId(other.getRowId());
- }
- if (other.hasLength()) {
- setLength(other.getLength());
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.Sticker) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional uint64 rowId = 1;
- private long rowId_ ;
- /**
- * optional uint64 rowId = 1;
- */
- public boolean hasRowId() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional uint64 rowId = 1;
- */
- public long getRowId() {
- return rowId_;
- }
- /**
- * optional uint64 rowId = 1;
- */
- public Builder setRowId(long value) {
- bitField0_ |= 0x00000001;
- rowId_ = value;
- onChanged();
- return this;
- }
- /**
- * optional uint64 rowId = 1;
- */
- public Builder clearRowId() {
- bitField0_ = (bitField0_ & ~0x00000001);
- rowId_ = 0L;
- onChanged();
- return this;
- }
-
- // optional uint32 length = 2;
- private int length_ ;
- /**
- * optional uint32 length = 2;
- */
- public boolean hasLength() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional uint32 length = 2;
- */
- public int getLength() {
- return length_;
- }
- /**
- * optional uint32 length = 2;
- */
- public Builder setLength(int value) {
- bitField0_ |= 0x00000002;
- length_ = value;
- onChanged();
- return this;
- }
- /**
- * optional uint32 length = 2;
- */
- public Builder clearLength() {
- bitField0_ = (bitField0_ & ~0x00000002);
- length_ = 0;
- onChanged();
- return this;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.Sticker)
- }
-
- static {
- defaultInstance = new Sticker(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.Sticker)
- }
-
- public interface AvatarOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional string name = 1;
- /**
- * optional string name = 1;
- */
- boolean hasName();
- /**
- * optional string name = 1;
- */
- java.lang.String getName();
- /**
- * optional string name = 1;
- */
- com.google.protobuf.ByteString
- getNameBytes();
-
- // optional uint32 length = 2;
- /**
- * optional uint32 length = 2;
- */
- boolean hasLength();
- /**
- * optional uint32 length = 2;
- */
- int getLength();
- }
- /**
- * Protobuf type {@code signal.Avatar}
- */
- public static final class Avatar extends
- com.google.protobuf.GeneratedMessage
- implements AvatarOrBuilder {
- // Use Avatar.newBuilder() to construct.
- private Avatar(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private Avatar(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final Avatar defaultInstance;
- public static Avatar getDefaultInstance() {
- return defaultInstance;
- }
-
- public Avatar getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private Avatar(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 10: {
- bitField0_ |= 0x00000001;
- name_ = input.readBytes();
- break;
- }
- case 16: {
- bitField0_ |= 0x00000002;
- length_ = input.readUInt32();
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Avatar_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Avatar_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar.class, org.thoughtcrime.securesms.backup.BackupProtos.Avatar.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public Avatar parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new Avatar(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- private int bitField0_;
- // optional string name = 1;
- public static final int NAME_FIELD_NUMBER = 1;
- private java.lang.Object name_;
- /**
- * optional string name = 1;
- */
- public boolean hasName() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional string name = 1;
- */
- public java.lang.String getName() {
- java.lang.Object ref = name_;
- if (ref instanceof java.lang.String) {
- return (java.lang.String) ref;
- } else {
- com.google.protobuf.ByteString bs =
- (com.google.protobuf.ByteString) ref;
- java.lang.String s = bs.toStringUtf8();
- if (bs.isValidUtf8()) {
- name_ = s;
- }
- return s;
- }
- }
- /**
- * optional string name = 1;
- */
- public com.google.protobuf.ByteString
- getNameBytes() {
- java.lang.Object ref = name_;
- if (ref instanceof java.lang.String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- name_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
-
- // optional uint32 length = 2;
- public static final int LENGTH_FIELD_NUMBER = 2;
- private int length_;
- /**
- * optional uint32 length = 2;
- */
- public boolean hasLength() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional uint32 length = 2;
- */
- public int getLength() {
- return length_;
- }
-
- private void initFields() {
- name_ = "";
- length_ = 0;
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeBytes(1, getNameBytes());
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- output.writeUInt32(2, length_);
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(1, getNameBytes());
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- size += com.google.protobuf.CodedOutputStream
- .computeUInt32Size(2, length_);
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Avatar parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.Avatar prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.Avatar}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.AvatarOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Avatar_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Avatar_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar.class, org.thoughtcrime.securesms.backup.BackupProtos.Avatar.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.Avatar.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- name_ = "";
- bitField0_ = (bitField0_ & ~0x00000001);
- length_ = 0;
- bitField0_ = (bitField0_ & ~0x00000002);
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Avatar_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Avatar getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.Avatar.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Avatar build() {
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Avatar buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar result = new org.thoughtcrime.securesms.backup.BackupProtos.Avatar(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- result.name_ = name_;
- if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
- to_bitField0_ |= 0x00000002;
- }
- result.length_ = length_;
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.Avatar) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.Avatar)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.Avatar other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.Avatar.getDefaultInstance()) return this;
- if (other.hasName()) {
- bitField0_ |= 0x00000001;
- name_ = other.name_;
- onChanged();
- }
- if (other.hasLength()) {
- setLength(other.getLength());
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.Avatar) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional string name = 1;
- private java.lang.Object name_ = "";
- /**
- * optional string name = 1;
- */
- public boolean hasName() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional string name = 1;
- */
- public java.lang.String getName() {
- java.lang.Object ref = name_;
- if (!(ref instanceof java.lang.String)) {
- java.lang.String s = ((com.google.protobuf.ByteString) ref)
- .toStringUtf8();
- name_ = s;
- return s;
- } else {
- return (java.lang.String) ref;
- }
- }
- /**
- * optional string name = 1;
- */
- public com.google.protobuf.ByteString
- getNameBytes() {
- java.lang.Object ref = name_;
- if (ref instanceof String) {
- com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8(
- (java.lang.String) ref);
- name_ = b;
- return b;
- } else {
- return (com.google.protobuf.ByteString) ref;
- }
- }
- /**
- * optional string name = 1;
- */
- public Builder setName(
- java.lang.String value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- name_ = value;
- onChanged();
- return this;
- }
- /**
- * optional string name = 1;
- */
- public Builder clearName() {
- bitField0_ = (bitField0_ & ~0x00000001);
- name_ = getDefaultInstance().getName();
- onChanged();
- return this;
- }
- /**
- * optional string name = 1;
- */
- public Builder setNameBytes(
- com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- name_ = value;
- onChanged();
- return this;
- }
-
- // optional uint32 length = 2;
- private int length_ ;
- /**
- * optional uint32 length = 2;
- */
- public boolean hasLength() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional uint32 length = 2;
- */
- public int getLength() {
- return length_;
- }
- /**
- * optional uint32 length = 2;
- */
- public Builder setLength(int value) {
- bitField0_ |= 0x00000002;
- length_ = value;
- onChanged();
- return this;
- }
- /**
- * optional uint32 length = 2;
- */
- public Builder clearLength() {
- bitField0_ = (bitField0_ & ~0x00000002);
- length_ = 0;
- onChanged();
- return this;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.Avatar)
- }
-
- static {
- defaultInstance = new Avatar(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.Avatar)
- }
-
- public interface DatabaseVersionOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional uint32 version = 1;
- /**
- * optional uint32 version = 1;
- */
- boolean hasVersion();
- /**
- * optional uint32 version = 1;
- */
- int getVersion();
- }
- /**
- * Protobuf type {@code signal.DatabaseVersion}
- */
- public static final class DatabaseVersion extends
- com.google.protobuf.GeneratedMessage
- implements DatabaseVersionOrBuilder {
- // Use DatabaseVersion.newBuilder() to construct.
- private DatabaseVersion(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private DatabaseVersion(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final DatabaseVersion defaultInstance;
- public static DatabaseVersion getDefaultInstance() {
- return defaultInstance;
- }
-
- public DatabaseVersion getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private DatabaseVersion(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 8: {
- bitField0_ |= 0x00000001;
- version_ = input.readUInt32();
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_DatabaseVersion_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_DatabaseVersion_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.class, org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public DatabaseVersion parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new DatabaseVersion(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- private int bitField0_;
- // optional uint32 version = 1;
- public static final int VERSION_FIELD_NUMBER = 1;
- private int version_;
- /**
- * optional uint32 version = 1;
- */
- public boolean hasVersion() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional uint32 version = 1;
- */
- public int getVersion() {
- return version_;
- }
-
- private void initFields() {
- version_ = 0;
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeUInt32(1, version_);
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeUInt32Size(1, version_);
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.DatabaseVersion}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersionOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_DatabaseVersion_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_DatabaseVersion_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.class, org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- version_ = 0;
- bitField0_ = (bitField0_ & ~0x00000001);
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_DatabaseVersion_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion build() {
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion result = new org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- result.version_ = version_;
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.getDefaultInstance()) return this;
- if (other.hasVersion()) {
- setVersion(other.getVersion());
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional uint32 version = 1;
- private int version_ ;
- /**
- * optional uint32 version = 1;
- */
- public boolean hasVersion() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional uint32 version = 1;
- */
- public int getVersion() {
- return version_;
- }
- /**
- * optional uint32 version = 1;
- */
- public Builder setVersion(int value) {
- bitField0_ |= 0x00000001;
- version_ = value;
- onChanged();
- return this;
- }
- /**
- * optional uint32 version = 1;
- */
- public Builder clearVersion() {
- bitField0_ = (bitField0_ & ~0x00000001);
- version_ = 0;
- onChanged();
- return this;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.DatabaseVersion)
- }
-
- static {
- defaultInstance = new DatabaseVersion(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.DatabaseVersion)
- }
-
- public interface HeaderOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional bytes iv = 1;
- /**
- * optional bytes iv = 1;
- */
- boolean hasIv();
- /**
- * optional bytes iv = 1;
- */
- com.google.protobuf.ByteString getIv();
-
- // optional bytes salt = 2;
- /**
- * optional bytes salt = 2;
- */
- boolean hasSalt();
- /**
- * optional bytes salt = 2;
- */
- com.google.protobuf.ByteString getSalt();
- }
- /**
- * Protobuf type {@code signal.Header}
- */
- public static final class Header extends
- com.google.protobuf.GeneratedMessage
- implements HeaderOrBuilder {
- // Use Header.newBuilder() to construct.
- private Header(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private Header(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final Header defaultInstance;
- public static Header getDefaultInstance() {
- return defaultInstance;
- }
-
- public Header getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private Header(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 10: {
- bitField0_ |= 0x00000001;
- iv_ = input.readBytes();
- break;
- }
- case 18: {
- bitField0_ |= 0x00000002;
- salt_ = input.readBytes();
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Header_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Header_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.Header.class, org.thoughtcrime.securesms.backup.BackupProtos.Header.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public Header parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new Header(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- private int bitField0_;
- // optional bytes iv = 1;
- public static final int IV_FIELD_NUMBER = 1;
- private com.google.protobuf.ByteString iv_;
- /**
- * optional bytes iv = 1;
- */
- public boolean hasIv() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional bytes iv = 1;
- */
- public com.google.protobuf.ByteString getIv() {
- return iv_;
- }
-
- // optional bytes salt = 2;
- public static final int SALT_FIELD_NUMBER = 2;
- private com.google.protobuf.ByteString salt_;
- /**
- * optional bytes salt = 2;
- */
- public boolean hasSalt() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional bytes salt = 2;
- */
- public com.google.protobuf.ByteString getSalt() {
- return salt_;
- }
-
- private void initFields() {
- iv_ = com.google.protobuf.ByteString.EMPTY;
- salt_ = com.google.protobuf.ByteString.EMPTY;
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeBytes(1, iv_);
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- output.writeBytes(2, salt_);
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(1, iv_);
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBytesSize(2, salt_);
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.Header parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.Header prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.Header}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.HeaderOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Header_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Header_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.Header.class, org.thoughtcrime.securesms.backup.BackupProtos.Header.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.Header.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- iv_ = com.google.protobuf.ByteString.EMPTY;
- bitField0_ = (bitField0_ & ~0x00000001);
- salt_ = com.google.protobuf.ByteString.EMPTY;
- bitField0_ = (bitField0_ & ~0x00000002);
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_Header_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Header getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.Header.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Header build() {
- org.thoughtcrime.securesms.backup.BackupProtos.Header result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.Header buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.Header result = new org.thoughtcrime.securesms.backup.BackupProtos.Header(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- result.iv_ = iv_;
- if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
- to_bitField0_ |= 0x00000002;
- }
- result.salt_ = salt_;
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.Header) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.Header)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.Header other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.Header.getDefaultInstance()) return this;
- if (other.hasIv()) {
- setIv(other.getIv());
- }
- if (other.hasSalt()) {
- setSalt(other.getSalt());
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.Header parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.Header) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional bytes iv = 1;
- private com.google.protobuf.ByteString iv_ = com.google.protobuf.ByteString.EMPTY;
- /**
- * optional bytes iv = 1;
- */
- public boolean hasIv() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional bytes iv = 1;
- */
- public com.google.protobuf.ByteString getIv() {
- return iv_;
- }
- /**
- * optional bytes iv = 1;
- */
- public Builder setIv(com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000001;
- iv_ = value;
- onChanged();
- return this;
- }
- /**
- * optional bytes iv = 1;
- */
- public Builder clearIv() {
- bitField0_ = (bitField0_ & ~0x00000001);
- iv_ = getDefaultInstance().getIv();
- onChanged();
- return this;
- }
-
- // optional bytes salt = 2;
- private com.google.protobuf.ByteString salt_ = com.google.protobuf.ByteString.EMPTY;
- /**
- * optional bytes salt = 2;
- */
- public boolean hasSalt() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional bytes salt = 2;
- */
- public com.google.protobuf.ByteString getSalt() {
- return salt_;
- }
- /**
- * optional bytes salt = 2;
- */
- public Builder setSalt(com.google.protobuf.ByteString value) {
- if (value == null) {
- throw new NullPointerException();
- }
- bitField0_ |= 0x00000002;
- salt_ = value;
- onChanged();
- return this;
- }
- /**
- * optional bytes salt = 2;
- */
- public Builder clearSalt() {
- bitField0_ = (bitField0_ & ~0x00000002);
- salt_ = getDefaultInstance().getSalt();
- onChanged();
- return this;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.Header)
- }
-
- static {
- defaultInstance = new Header(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.Header)
- }
-
- public interface BackupFrameOrBuilder
- extends com.google.protobuf.MessageOrBuilder {
-
- // optional .signal.Header header = 1;
- /**
- * optional .signal.Header header = 1;
- */
- boolean hasHeader();
- /**
- * optional .signal.Header header = 1;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.Header getHeader();
- /**
- * optional .signal.Header header = 1;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.HeaderOrBuilder getHeaderOrBuilder();
-
- // optional .signal.SqlStatement statement = 2;
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- boolean hasStatement();
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement getStatement();
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatementOrBuilder getStatementOrBuilder();
-
- // optional .signal.SharedPreference preference = 3;
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- boolean hasPreference();
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference getPreference();
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreferenceOrBuilder getPreferenceOrBuilder();
-
- // optional .signal.Attachment attachment = 4;
- /**
- * optional .signal.Attachment attachment = 4;
- */
- boolean hasAttachment();
- /**
- * optional .signal.Attachment attachment = 4;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment getAttachment();
- /**
- * optional .signal.Attachment attachment = 4;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.AttachmentOrBuilder getAttachmentOrBuilder();
-
- // optional .signal.DatabaseVersion version = 5;
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- boolean hasVersion();
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion getVersion();
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersionOrBuilder getVersionOrBuilder();
-
- // optional bool end = 6;
- /**
- * optional bool end = 6;
- */
- boolean hasEnd();
- /**
- * optional bool end = 6;
- */
- boolean getEnd();
-
- // optional .signal.Avatar avatar = 7;
- /**
- * optional .signal.Avatar avatar = 7;
- */
- boolean hasAvatar();
- /**
- * optional .signal.Avatar avatar = 7;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar getAvatar();
- /**
- * optional .signal.Avatar avatar = 7;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.AvatarOrBuilder getAvatarOrBuilder();
-
- // optional .signal.Sticker sticker = 8;
- /**
- * optional .signal.Sticker sticker = 8;
- */
- boolean hasSticker();
- /**
- * optional .signal.Sticker sticker = 8;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker getSticker();
- /**
- * optional .signal.Sticker sticker = 8;
- */
- org.thoughtcrime.securesms.backup.BackupProtos.StickerOrBuilder getStickerOrBuilder();
- }
- /**
- * Protobuf type {@code signal.BackupFrame}
- */
- public static final class BackupFrame extends
- com.google.protobuf.GeneratedMessage
- implements BackupFrameOrBuilder {
- // Use BackupFrame.newBuilder() to construct.
- private BackupFrame(com.google.protobuf.GeneratedMessage.Builder> builder) {
- super(builder);
- this.unknownFields = builder.getUnknownFields();
- }
- private BackupFrame(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
-
- private static final BackupFrame defaultInstance;
- public static BackupFrame getDefaultInstance() {
- return defaultInstance;
- }
-
- public BackupFrame getDefaultInstanceForType() {
- return defaultInstance;
- }
-
- private final com.google.protobuf.UnknownFieldSet unknownFields;
- @java.lang.Override
- public final com.google.protobuf.UnknownFieldSet
- getUnknownFields() {
- return this.unknownFields;
- }
- private BackupFrame(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- initFields();
- int mutable_bitField0_ = 0;
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder();
- try {
- boolean done = false;
- while (!done) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- done = true;
- break;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- done = true;
- }
- break;
- }
- case 10: {
- org.thoughtcrime.securesms.backup.BackupProtos.Header.Builder subBuilder = null;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- subBuilder = header_.toBuilder();
- }
- header_ = input.readMessage(org.thoughtcrime.securesms.backup.BackupProtos.Header.PARSER, extensionRegistry);
- if (subBuilder != null) {
- subBuilder.mergeFrom(header_);
- header_ = subBuilder.buildPartial();
- }
- bitField0_ |= 0x00000001;
- break;
- }
- case 18: {
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.Builder subBuilder = null;
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- subBuilder = statement_.toBuilder();
- }
- statement_ = input.readMessage(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.PARSER, extensionRegistry);
- if (subBuilder != null) {
- subBuilder.mergeFrom(statement_);
- statement_ = subBuilder.buildPartial();
- }
- bitField0_ |= 0x00000002;
- break;
- }
- case 26: {
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.Builder subBuilder = null;
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- subBuilder = preference_.toBuilder();
- }
- preference_ = input.readMessage(org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.PARSER, extensionRegistry);
- if (subBuilder != null) {
- subBuilder.mergeFrom(preference_);
- preference_ = subBuilder.buildPartial();
- }
- bitField0_ |= 0x00000004;
- break;
- }
- case 34: {
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment.Builder subBuilder = null;
- if (((bitField0_ & 0x00000008) == 0x00000008)) {
- subBuilder = attachment_.toBuilder();
- }
- attachment_ = input.readMessage(org.thoughtcrime.securesms.backup.BackupProtos.Attachment.PARSER, extensionRegistry);
- if (subBuilder != null) {
- subBuilder.mergeFrom(attachment_);
- attachment_ = subBuilder.buildPartial();
- }
- bitField0_ |= 0x00000008;
- break;
- }
- case 42: {
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.Builder subBuilder = null;
- if (((bitField0_ & 0x00000010) == 0x00000010)) {
- subBuilder = version_.toBuilder();
- }
- version_ = input.readMessage(org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.PARSER, extensionRegistry);
- if (subBuilder != null) {
- subBuilder.mergeFrom(version_);
- version_ = subBuilder.buildPartial();
- }
- bitField0_ |= 0x00000010;
- break;
- }
- case 48: {
- bitField0_ |= 0x00000020;
- end_ = input.readBool();
- break;
- }
- case 58: {
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar.Builder subBuilder = null;
- if (((bitField0_ & 0x00000040) == 0x00000040)) {
- subBuilder = avatar_.toBuilder();
- }
- avatar_ = input.readMessage(org.thoughtcrime.securesms.backup.BackupProtos.Avatar.PARSER, extensionRegistry);
- if (subBuilder != null) {
- subBuilder.mergeFrom(avatar_);
- avatar_ = subBuilder.buildPartial();
- }
- bitField0_ |= 0x00000040;
- break;
- }
- case 66: {
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker.Builder subBuilder = null;
- if (((bitField0_ & 0x00000080) == 0x00000080)) {
- subBuilder = sticker_.toBuilder();
- }
- sticker_ = input.readMessage(org.thoughtcrime.securesms.backup.BackupProtos.Sticker.PARSER, extensionRegistry);
- if (subBuilder != null) {
- subBuilder.mergeFrom(sticker_);
- sticker_ = subBuilder.buildPartial();
- }
- bitField0_ |= 0x00000080;
- break;
- }
- }
- }
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- throw e.setUnfinishedMessage(this);
- } catch (java.io.IOException e) {
- throw new com.google.protobuf.InvalidProtocolBufferException(
- e.getMessage()).setUnfinishedMessage(this);
- } finally {
- this.unknownFields = unknownFields.build();
- makeExtensionsImmutable();
- }
- }
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_BackupFrame_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_BackupFrame_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame.class, org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame.Builder.class);
- }
-
- public static com.google.protobuf.Parser PARSER =
- new com.google.protobuf.AbstractParser() {
- public BackupFrame parsePartialFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return new BackupFrame(input, extensionRegistry);
- }
- };
-
- @java.lang.Override
- public com.google.protobuf.Parser getParserForType() {
- return PARSER;
- }
-
- private int bitField0_;
- // optional .signal.Header header = 1;
- public static final int HEADER_FIELD_NUMBER = 1;
- private org.thoughtcrime.securesms.backup.BackupProtos.Header header_;
- /**
- * optional .signal.Header header = 1;
- */
- public boolean hasHeader() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional .signal.Header header = 1;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Header getHeader() {
- return header_;
- }
- /**
- * optional .signal.Header header = 1;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.HeaderOrBuilder getHeaderOrBuilder() {
- return header_;
- }
-
- // optional .signal.SqlStatement statement = 2;
- public static final int STATEMENT_FIELD_NUMBER = 2;
- private org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement statement_;
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public boolean hasStatement() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement getStatement() {
- return statement_;
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatementOrBuilder getStatementOrBuilder() {
- return statement_;
- }
-
- // optional .signal.SharedPreference preference = 3;
- public static final int PREFERENCE_FIELD_NUMBER = 3;
- private org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference preference_;
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public boolean hasPreference() {
- return ((bitField0_ & 0x00000004) == 0x00000004);
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference getPreference() {
- return preference_;
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SharedPreferenceOrBuilder getPreferenceOrBuilder() {
- return preference_;
- }
-
- // optional .signal.Attachment attachment = 4;
- public static final int ATTACHMENT_FIELD_NUMBER = 4;
- private org.thoughtcrime.securesms.backup.BackupProtos.Attachment attachment_;
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public boolean hasAttachment() {
- return ((bitField0_ & 0x00000008) == 0x00000008);
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Attachment getAttachment() {
- return attachment_;
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.AttachmentOrBuilder getAttachmentOrBuilder() {
- return attachment_;
- }
-
- // optional .signal.DatabaseVersion version = 5;
- public static final int VERSION_FIELD_NUMBER = 5;
- private org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion version_;
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public boolean hasVersion() {
- return ((bitField0_ & 0x00000010) == 0x00000010);
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion getVersion() {
- return version_;
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersionOrBuilder getVersionOrBuilder() {
- return version_;
- }
-
- // optional bool end = 6;
- public static final int END_FIELD_NUMBER = 6;
- private boolean end_;
- /**
- * optional bool end = 6;
- */
- public boolean hasEnd() {
- return ((bitField0_ & 0x00000020) == 0x00000020);
- }
- /**
- * optional bool end = 6;
- */
- public boolean getEnd() {
- return end_;
- }
-
- // optional .signal.Avatar avatar = 7;
- public static final int AVATAR_FIELD_NUMBER = 7;
- private org.thoughtcrime.securesms.backup.BackupProtos.Avatar avatar_;
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public boolean hasAvatar() {
- return ((bitField0_ & 0x00000040) == 0x00000040);
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Avatar getAvatar() {
- return avatar_;
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.AvatarOrBuilder getAvatarOrBuilder() {
- return avatar_;
- }
-
- // optional .signal.Sticker sticker = 8;
- public static final int STICKER_FIELD_NUMBER = 8;
- private org.thoughtcrime.securesms.backup.BackupProtos.Sticker sticker_;
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public boolean hasSticker() {
- return ((bitField0_ & 0x00000080) == 0x00000080);
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Sticker getSticker() {
- return sticker_;
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.StickerOrBuilder getStickerOrBuilder() {
- return sticker_;
- }
-
- private void initFields() {
- header_ = org.thoughtcrime.securesms.backup.BackupProtos.Header.getDefaultInstance();
- statement_ = org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.getDefaultInstance();
- preference_ = org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.getDefaultInstance();
- attachment_ = org.thoughtcrime.securesms.backup.BackupProtos.Attachment.getDefaultInstance();
- version_ = org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.getDefaultInstance();
- end_ = false;
- avatar_ = org.thoughtcrime.securesms.backup.BackupProtos.Avatar.getDefaultInstance();
- sticker_ = org.thoughtcrime.securesms.backup.BackupProtos.Sticker.getDefaultInstance();
- }
- private byte memoizedIsInitialized = -1;
- public final boolean isInitialized() {
- byte isInitialized = memoizedIsInitialized;
- if (isInitialized != -1) return isInitialized == 1;
-
- memoizedIsInitialized = 1;
- return true;
- }
-
- public void writeTo(com.google.protobuf.CodedOutputStream output)
- throws java.io.IOException {
- getSerializedSize();
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- output.writeMessage(1, header_);
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- output.writeMessage(2, statement_);
- }
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- output.writeMessage(3, preference_);
- }
- if (((bitField0_ & 0x00000008) == 0x00000008)) {
- output.writeMessage(4, attachment_);
- }
- if (((bitField0_ & 0x00000010) == 0x00000010)) {
- output.writeMessage(5, version_);
- }
- if (((bitField0_ & 0x00000020) == 0x00000020)) {
- output.writeBool(6, end_);
- }
- if (((bitField0_ & 0x00000040) == 0x00000040)) {
- output.writeMessage(7, avatar_);
- }
- if (((bitField0_ & 0x00000080) == 0x00000080)) {
- output.writeMessage(8, sticker_);
- }
- getUnknownFields().writeTo(output);
- }
-
- private int memoizedSerializedSize = -1;
- public int getSerializedSize() {
- int size = memoizedSerializedSize;
- if (size != -1) return size;
-
- size = 0;
- if (((bitField0_ & 0x00000001) == 0x00000001)) {
- size += com.google.protobuf.CodedOutputStream
- .computeMessageSize(1, header_);
- }
- if (((bitField0_ & 0x00000002) == 0x00000002)) {
- size += com.google.protobuf.CodedOutputStream
- .computeMessageSize(2, statement_);
- }
- if (((bitField0_ & 0x00000004) == 0x00000004)) {
- size += com.google.protobuf.CodedOutputStream
- .computeMessageSize(3, preference_);
- }
- if (((bitField0_ & 0x00000008) == 0x00000008)) {
- size += com.google.protobuf.CodedOutputStream
- .computeMessageSize(4, attachment_);
- }
- if (((bitField0_ & 0x00000010) == 0x00000010)) {
- size += com.google.protobuf.CodedOutputStream
- .computeMessageSize(5, version_);
- }
- if (((bitField0_ & 0x00000020) == 0x00000020)) {
- size += com.google.protobuf.CodedOutputStream
- .computeBoolSize(6, end_);
- }
- if (((bitField0_ & 0x00000040) == 0x00000040)) {
- size += com.google.protobuf.CodedOutputStream
- .computeMessageSize(7, avatar_);
- }
- if (((bitField0_ & 0x00000080) == 0x00000080)) {
- size += com.google.protobuf.CodedOutputStream
- .computeMessageSize(8, sticker_);
- }
- size += getUnknownFields().getSerializedSize();
- memoizedSerializedSize = size;
- return size;
- }
-
- private static final long serialVersionUID = 0L;
- @java.lang.Override
- protected java.lang.Object writeReplace()
- throws java.io.ObjectStreamException {
- return super.writeReplace();
- }
-
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseFrom(
- com.google.protobuf.ByteString data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseFrom(
- com.google.protobuf.ByteString data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseFrom(byte[] data)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseFrom(
- byte[] data,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws com.google.protobuf.InvalidProtocolBufferException {
- return PARSER.parseFrom(data, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseDelimitedFrom(java.io.InputStream input)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseDelimitedFrom(
- java.io.InputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseDelimitedFrom(input, extensionRegistry);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseFrom(
- com.google.protobuf.CodedInputStream input)
- throws java.io.IOException {
- return PARSER.parseFrom(input);
- }
- public static org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parseFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- return PARSER.parseFrom(input, extensionRegistry);
- }
-
- public static Builder newBuilder() { return Builder.create(); }
- public Builder newBuilderForType() { return newBuilder(); }
- public static Builder newBuilder(org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame prototype) {
- return newBuilder().mergeFrom(prototype);
- }
- public Builder toBuilder() { return newBuilder(this); }
-
- @java.lang.Override
- protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- Builder builder = new Builder(parent);
- return builder;
- }
- /**
- * Protobuf type {@code signal.BackupFrame}
- */
- public static final class Builder extends
- com.google.protobuf.GeneratedMessage.Builder
- implements org.thoughtcrime.securesms.backup.BackupProtos.BackupFrameOrBuilder {
- public static final com.google.protobuf.Descriptors.Descriptor
- getDescriptor() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_BackupFrame_descriptor;
- }
-
- protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internalGetFieldAccessorTable() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_BackupFrame_fieldAccessorTable
- .ensureFieldAccessorsInitialized(
- org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame.class, org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame.Builder.class);
- }
-
- // Construct using org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame.newBuilder()
- private Builder() {
- maybeForceBuilderInitialization();
- }
-
- private Builder(
- com.google.protobuf.GeneratedMessage.BuilderParent parent) {
- super(parent);
- maybeForceBuilderInitialization();
- }
- private void maybeForceBuilderInitialization() {
- if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
- getHeaderFieldBuilder();
- getStatementFieldBuilder();
- getPreferenceFieldBuilder();
- getAttachmentFieldBuilder();
- getVersionFieldBuilder();
- getAvatarFieldBuilder();
- getStickerFieldBuilder();
- }
- }
- private static Builder create() {
- return new Builder();
- }
-
- public Builder clear() {
- super.clear();
- if (headerBuilder_ == null) {
- header_ = org.thoughtcrime.securesms.backup.BackupProtos.Header.getDefaultInstance();
- } else {
- headerBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000001);
- if (statementBuilder_ == null) {
- statement_ = org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.getDefaultInstance();
- } else {
- statementBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000002);
- if (preferenceBuilder_ == null) {
- preference_ = org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.getDefaultInstance();
- } else {
- preferenceBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000004);
- if (attachmentBuilder_ == null) {
- attachment_ = org.thoughtcrime.securesms.backup.BackupProtos.Attachment.getDefaultInstance();
- } else {
- attachmentBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000008);
- if (versionBuilder_ == null) {
- version_ = org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.getDefaultInstance();
- } else {
- versionBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000010);
- end_ = false;
- bitField0_ = (bitField0_ & ~0x00000020);
- if (avatarBuilder_ == null) {
- avatar_ = org.thoughtcrime.securesms.backup.BackupProtos.Avatar.getDefaultInstance();
- } else {
- avatarBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000040);
- if (stickerBuilder_ == null) {
- sticker_ = org.thoughtcrime.securesms.backup.BackupProtos.Sticker.getDefaultInstance();
- } else {
- stickerBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000080);
- return this;
- }
-
- public Builder clone() {
- return create().mergeFrom(buildPartial());
- }
-
- public com.google.protobuf.Descriptors.Descriptor
- getDescriptorForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.internal_static_signal_BackupFrame_descriptor;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame getDefaultInstanceForType() {
- return org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame.getDefaultInstance();
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame build() {
- org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(result);
- }
- return result;
- }
-
- public org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame buildPartial() {
- org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame result = new org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame(this);
- int from_bitField0_ = bitField0_;
- int to_bitField0_ = 0;
- if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
- to_bitField0_ |= 0x00000001;
- }
- if (headerBuilder_ == null) {
- result.header_ = header_;
- } else {
- result.header_ = headerBuilder_.build();
- }
- if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
- to_bitField0_ |= 0x00000002;
- }
- if (statementBuilder_ == null) {
- result.statement_ = statement_;
- } else {
- result.statement_ = statementBuilder_.build();
- }
- if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
- to_bitField0_ |= 0x00000004;
- }
- if (preferenceBuilder_ == null) {
- result.preference_ = preference_;
- } else {
- result.preference_ = preferenceBuilder_.build();
- }
- if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
- to_bitField0_ |= 0x00000008;
- }
- if (attachmentBuilder_ == null) {
- result.attachment_ = attachment_;
- } else {
- result.attachment_ = attachmentBuilder_.build();
- }
- if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
- to_bitField0_ |= 0x00000010;
- }
- if (versionBuilder_ == null) {
- result.version_ = version_;
- } else {
- result.version_ = versionBuilder_.build();
- }
- if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
- to_bitField0_ |= 0x00000020;
- }
- result.end_ = end_;
- if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
- to_bitField0_ |= 0x00000040;
- }
- if (avatarBuilder_ == null) {
- result.avatar_ = avatar_;
- } else {
- result.avatar_ = avatarBuilder_.build();
- }
- if (((from_bitField0_ & 0x00000080) == 0x00000080)) {
- to_bitField0_ |= 0x00000080;
- }
- if (stickerBuilder_ == null) {
- result.sticker_ = sticker_;
- } else {
- result.sticker_ = stickerBuilder_.build();
- }
- result.bitField0_ = to_bitField0_;
- onBuilt();
- return result;
- }
-
- public Builder mergeFrom(com.google.protobuf.Message other) {
- if (other instanceof org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame) {
- return mergeFrom((org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame)other);
- } else {
- super.mergeFrom(other);
- return this;
- }
- }
-
- public Builder mergeFrom(org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame other) {
- if (other == org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame.getDefaultInstance()) return this;
- if (other.hasHeader()) {
- mergeHeader(other.getHeader());
- }
- if (other.hasStatement()) {
- mergeStatement(other.getStatement());
- }
- if (other.hasPreference()) {
- mergePreference(other.getPreference());
- }
- if (other.hasAttachment()) {
- mergeAttachment(other.getAttachment());
- }
- if (other.hasVersion()) {
- mergeVersion(other.getVersion());
- }
- if (other.hasEnd()) {
- setEnd(other.getEnd());
- }
- if (other.hasAvatar()) {
- mergeAvatar(other.getAvatar());
- }
- if (other.hasSticker()) {
- mergeSticker(other.getSticker());
- }
- this.mergeUnknownFields(other.getUnknownFields());
- return this;
- }
-
- public final boolean isInitialized() {
- return true;
- }
-
- public Builder mergeFrom(
- com.google.protobuf.CodedInputStream input,
- com.google.protobuf.ExtensionRegistryLite extensionRegistry)
- throws java.io.IOException {
- org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame parsedMessage = null;
- try {
- parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
- } catch (com.google.protobuf.InvalidProtocolBufferException e) {
- parsedMessage = (org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame) e.getUnfinishedMessage();
- throw e;
- } finally {
- if (parsedMessage != null) {
- mergeFrom(parsedMessage);
- }
- }
- return this;
- }
- private int bitField0_;
-
- // optional .signal.Header header = 1;
- private org.thoughtcrime.securesms.backup.BackupProtos.Header header_ = org.thoughtcrime.securesms.backup.BackupProtos.Header.getDefaultInstance();
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Header, org.thoughtcrime.securesms.backup.BackupProtos.Header.Builder, org.thoughtcrime.securesms.backup.BackupProtos.HeaderOrBuilder> headerBuilder_;
- /**
- * optional .signal.Header header = 1;
- */
- public boolean hasHeader() {
- return ((bitField0_ & 0x00000001) == 0x00000001);
- }
- /**
- * optional .signal.Header header = 1;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Header getHeader() {
- if (headerBuilder_ == null) {
- return header_;
- } else {
- return headerBuilder_.getMessage();
- }
- }
- /**
- * optional .signal.Header header = 1;
- */
- public Builder setHeader(org.thoughtcrime.securesms.backup.BackupProtos.Header value) {
- if (headerBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- header_ = value;
- onChanged();
- } else {
- headerBuilder_.setMessage(value);
- }
- bitField0_ |= 0x00000001;
- return this;
- }
- /**
- * optional .signal.Header header = 1;
- */
- public Builder setHeader(
- org.thoughtcrime.securesms.backup.BackupProtos.Header.Builder builderForValue) {
- if (headerBuilder_ == null) {
- header_ = builderForValue.build();
- onChanged();
- } else {
- headerBuilder_.setMessage(builderForValue.build());
- }
- bitField0_ |= 0x00000001;
- return this;
- }
- /**
- * optional .signal.Header header = 1;
- */
- public Builder mergeHeader(org.thoughtcrime.securesms.backup.BackupProtos.Header value) {
- if (headerBuilder_ == null) {
- if (((bitField0_ & 0x00000001) == 0x00000001) &&
- header_ != org.thoughtcrime.securesms.backup.BackupProtos.Header.getDefaultInstance()) {
- header_ =
- org.thoughtcrime.securesms.backup.BackupProtos.Header.newBuilder(header_).mergeFrom(value).buildPartial();
- } else {
- header_ = value;
- }
- onChanged();
- } else {
- headerBuilder_.mergeFrom(value);
- }
- bitField0_ |= 0x00000001;
- return this;
- }
- /**
- * optional .signal.Header header = 1;
- */
- public Builder clearHeader() {
- if (headerBuilder_ == null) {
- header_ = org.thoughtcrime.securesms.backup.BackupProtos.Header.getDefaultInstance();
- onChanged();
- } else {
- headerBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000001);
- return this;
- }
- /**
- * optional .signal.Header header = 1;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Header.Builder getHeaderBuilder() {
- bitField0_ |= 0x00000001;
- onChanged();
- return getHeaderFieldBuilder().getBuilder();
- }
- /**
- * optional .signal.Header header = 1;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.HeaderOrBuilder getHeaderOrBuilder() {
- if (headerBuilder_ != null) {
- return headerBuilder_.getMessageOrBuilder();
- } else {
- return header_;
- }
- }
- /**
- * optional .signal.Header header = 1;
- */
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Header, org.thoughtcrime.securesms.backup.BackupProtos.Header.Builder, org.thoughtcrime.securesms.backup.BackupProtos.HeaderOrBuilder>
- getHeaderFieldBuilder() {
- if (headerBuilder_ == null) {
- headerBuilder_ = new com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Header, org.thoughtcrime.securesms.backup.BackupProtos.Header.Builder, org.thoughtcrime.securesms.backup.BackupProtos.HeaderOrBuilder>(
- header_,
- getParentForChildren(),
- isClean());
- header_ = null;
- }
- return headerBuilder_;
- }
-
- // optional .signal.SqlStatement statement = 2;
- private org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement statement_ = org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.getDefaultInstance();
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatementOrBuilder> statementBuilder_;
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public boolean hasStatement() {
- return ((bitField0_ & 0x00000002) == 0x00000002);
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement getStatement() {
- if (statementBuilder_ == null) {
- return statement_;
- } else {
- return statementBuilder_.getMessage();
- }
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public Builder setStatement(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement value) {
- if (statementBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- statement_ = value;
- onChanged();
- } else {
- statementBuilder_.setMessage(value);
- }
- bitField0_ |= 0x00000002;
- return this;
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public Builder setStatement(
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.Builder builderForValue) {
- if (statementBuilder_ == null) {
- statement_ = builderForValue.build();
- onChanged();
- } else {
- statementBuilder_.setMessage(builderForValue.build());
- }
- bitField0_ |= 0x00000002;
- return this;
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public Builder mergeStatement(org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement value) {
- if (statementBuilder_ == null) {
- if (((bitField0_ & 0x00000002) == 0x00000002) &&
- statement_ != org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.getDefaultInstance()) {
- statement_ =
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.newBuilder(statement_).mergeFrom(value).buildPartial();
- } else {
- statement_ = value;
- }
- onChanged();
- } else {
- statementBuilder_.mergeFrom(value);
- }
- bitField0_ |= 0x00000002;
- return this;
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public Builder clearStatement() {
- if (statementBuilder_ == null) {
- statement_ = org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.getDefaultInstance();
- onChanged();
- } else {
- statementBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000002);
- return this;
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.Builder getStatementBuilder() {
- bitField0_ |= 0x00000002;
- onChanged();
- return getStatementFieldBuilder().getBuilder();
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SqlStatementOrBuilder getStatementOrBuilder() {
- if (statementBuilder_ != null) {
- return statementBuilder_.getMessageOrBuilder();
- } else {
- return statement_;
- }
- }
- /**
- * optional .signal.SqlStatement statement = 2;
- */
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatementOrBuilder>
- getStatementFieldBuilder() {
- if (statementBuilder_ == null) {
- statementBuilder_ = new com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SqlStatementOrBuilder>(
- statement_,
- getParentForChildren(),
- isClean());
- statement_ = null;
- }
- return statementBuilder_;
- }
-
- // optional .signal.SharedPreference preference = 3;
- private org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference preference_ = org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.getDefaultInstance();
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference, org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SharedPreferenceOrBuilder> preferenceBuilder_;
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public boolean hasPreference() {
- return ((bitField0_ & 0x00000004) == 0x00000004);
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference getPreference() {
- if (preferenceBuilder_ == null) {
- return preference_;
- } else {
- return preferenceBuilder_.getMessage();
- }
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public Builder setPreference(org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference value) {
- if (preferenceBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- preference_ = value;
- onChanged();
- } else {
- preferenceBuilder_.setMessage(value);
- }
- bitField0_ |= 0x00000004;
- return this;
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public Builder setPreference(
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.Builder builderForValue) {
- if (preferenceBuilder_ == null) {
- preference_ = builderForValue.build();
- onChanged();
- } else {
- preferenceBuilder_.setMessage(builderForValue.build());
- }
- bitField0_ |= 0x00000004;
- return this;
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public Builder mergePreference(org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference value) {
- if (preferenceBuilder_ == null) {
- if (((bitField0_ & 0x00000004) == 0x00000004) &&
- preference_ != org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.getDefaultInstance()) {
- preference_ =
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.newBuilder(preference_).mergeFrom(value).buildPartial();
- } else {
- preference_ = value;
- }
- onChanged();
- } else {
- preferenceBuilder_.mergeFrom(value);
- }
- bitField0_ |= 0x00000004;
- return this;
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public Builder clearPreference() {
- if (preferenceBuilder_ == null) {
- preference_ = org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.getDefaultInstance();
- onChanged();
- } else {
- preferenceBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000004);
- return this;
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.Builder getPreferenceBuilder() {
- bitField0_ |= 0x00000004;
- onChanged();
- return getPreferenceFieldBuilder().getBuilder();
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.SharedPreferenceOrBuilder getPreferenceOrBuilder() {
- if (preferenceBuilder_ != null) {
- return preferenceBuilder_.getMessageOrBuilder();
- } else {
- return preference_;
- }
- }
- /**
- * optional .signal.SharedPreference preference = 3;
- */
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference, org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SharedPreferenceOrBuilder>
- getPreferenceFieldBuilder() {
- if (preferenceBuilder_ == null) {
- preferenceBuilder_ = new com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference, org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference.Builder, org.thoughtcrime.securesms.backup.BackupProtos.SharedPreferenceOrBuilder>(
- preference_,
- getParentForChildren(),
- isClean());
- preference_ = null;
- }
- return preferenceBuilder_;
- }
-
- // optional .signal.Attachment attachment = 4;
- private org.thoughtcrime.securesms.backup.BackupProtos.Attachment attachment_ = org.thoughtcrime.securesms.backup.BackupProtos.Attachment.getDefaultInstance();
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment, org.thoughtcrime.securesms.backup.BackupProtos.Attachment.Builder, org.thoughtcrime.securesms.backup.BackupProtos.AttachmentOrBuilder> attachmentBuilder_;
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public boolean hasAttachment() {
- return ((bitField0_ & 0x00000008) == 0x00000008);
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Attachment getAttachment() {
- if (attachmentBuilder_ == null) {
- return attachment_;
- } else {
- return attachmentBuilder_.getMessage();
- }
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public Builder setAttachment(org.thoughtcrime.securesms.backup.BackupProtos.Attachment value) {
- if (attachmentBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- attachment_ = value;
- onChanged();
- } else {
- attachmentBuilder_.setMessage(value);
- }
- bitField0_ |= 0x00000008;
- return this;
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public Builder setAttachment(
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment.Builder builderForValue) {
- if (attachmentBuilder_ == null) {
- attachment_ = builderForValue.build();
- onChanged();
- } else {
- attachmentBuilder_.setMessage(builderForValue.build());
- }
- bitField0_ |= 0x00000008;
- return this;
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public Builder mergeAttachment(org.thoughtcrime.securesms.backup.BackupProtos.Attachment value) {
- if (attachmentBuilder_ == null) {
- if (((bitField0_ & 0x00000008) == 0x00000008) &&
- attachment_ != org.thoughtcrime.securesms.backup.BackupProtos.Attachment.getDefaultInstance()) {
- attachment_ =
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment.newBuilder(attachment_).mergeFrom(value).buildPartial();
- } else {
- attachment_ = value;
- }
- onChanged();
- } else {
- attachmentBuilder_.mergeFrom(value);
- }
- bitField0_ |= 0x00000008;
- return this;
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public Builder clearAttachment() {
- if (attachmentBuilder_ == null) {
- attachment_ = org.thoughtcrime.securesms.backup.BackupProtos.Attachment.getDefaultInstance();
- onChanged();
- } else {
- attachmentBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000008);
- return this;
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Attachment.Builder getAttachmentBuilder() {
- bitField0_ |= 0x00000008;
- onChanged();
- return getAttachmentFieldBuilder().getBuilder();
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.AttachmentOrBuilder getAttachmentOrBuilder() {
- if (attachmentBuilder_ != null) {
- return attachmentBuilder_.getMessageOrBuilder();
- } else {
- return attachment_;
- }
- }
- /**
- * optional .signal.Attachment attachment = 4;
- */
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment, org.thoughtcrime.securesms.backup.BackupProtos.Attachment.Builder, org.thoughtcrime.securesms.backup.BackupProtos.AttachmentOrBuilder>
- getAttachmentFieldBuilder() {
- if (attachmentBuilder_ == null) {
- attachmentBuilder_ = new com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Attachment, org.thoughtcrime.securesms.backup.BackupProtos.Attachment.Builder, org.thoughtcrime.securesms.backup.BackupProtos.AttachmentOrBuilder>(
- attachment_,
- getParentForChildren(),
- isClean());
- attachment_ = null;
- }
- return attachmentBuilder_;
- }
-
- // optional .signal.DatabaseVersion version = 5;
- private org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion version_ = org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.getDefaultInstance();
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion, org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.Builder, org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersionOrBuilder> versionBuilder_;
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public boolean hasVersion() {
- return ((bitField0_ & 0x00000010) == 0x00000010);
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion getVersion() {
- if (versionBuilder_ == null) {
- return version_;
- } else {
- return versionBuilder_.getMessage();
- }
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public Builder setVersion(org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion value) {
- if (versionBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- version_ = value;
- onChanged();
- } else {
- versionBuilder_.setMessage(value);
- }
- bitField0_ |= 0x00000010;
- return this;
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public Builder setVersion(
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.Builder builderForValue) {
- if (versionBuilder_ == null) {
- version_ = builderForValue.build();
- onChanged();
- } else {
- versionBuilder_.setMessage(builderForValue.build());
- }
- bitField0_ |= 0x00000010;
- return this;
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public Builder mergeVersion(org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion value) {
- if (versionBuilder_ == null) {
- if (((bitField0_ & 0x00000010) == 0x00000010) &&
- version_ != org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.getDefaultInstance()) {
- version_ =
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.newBuilder(version_).mergeFrom(value).buildPartial();
- } else {
- version_ = value;
- }
- onChanged();
- } else {
- versionBuilder_.mergeFrom(value);
- }
- bitField0_ |= 0x00000010;
- return this;
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public Builder clearVersion() {
- if (versionBuilder_ == null) {
- version_ = org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.getDefaultInstance();
- onChanged();
- } else {
- versionBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000010);
- return this;
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.Builder getVersionBuilder() {
- bitField0_ |= 0x00000010;
- onChanged();
- return getVersionFieldBuilder().getBuilder();
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersionOrBuilder getVersionOrBuilder() {
- if (versionBuilder_ != null) {
- return versionBuilder_.getMessageOrBuilder();
- } else {
- return version_;
- }
- }
- /**
- * optional .signal.DatabaseVersion version = 5;
- */
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion, org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.Builder, org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersionOrBuilder>
- getVersionFieldBuilder() {
- if (versionBuilder_ == null) {
- versionBuilder_ = new com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion, org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion.Builder, org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersionOrBuilder>(
- version_,
- getParentForChildren(),
- isClean());
- version_ = null;
- }
- return versionBuilder_;
- }
-
- // optional bool end = 6;
- private boolean end_ ;
- /**
- * optional bool end = 6;
- */
- public boolean hasEnd() {
- return ((bitField0_ & 0x00000020) == 0x00000020);
- }
- /**
- * optional bool end = 6;
- */
- public boolean getEnd() {
- return end_;
- }
- /**
- * optional bool end = 6;
- */
- public Builder setEnd(boolean value) {
- bitField0_ |= 0x00000020;
- end_ = value;
- onChanged();
- return this;
- }
- /**
- * optional bool end = 6;
- */
- public Builder clearEnd() {
- bitField0_ = (bitField0_ & ~0x00000020);
- end_ = false;
- onChanged();
- return this;
- }
-
- // optional .signal.Avatar avatar = 7;
- private org.thoughtcrime.securesms.backup.BackupProtos.Avatar avatar_ = org.thoughtcrime.securesms.backup.BackupProtos.Avatar.getDefaultInstance();
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar, org.thoughtcrime.securesms.backup.BackupProtos.Avatar.Builder, org.thoughtcrime.securesms.backup.BackupProtos.AvatarOrBuilder> avatarBuilder_;
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public boolean hasAvatar() {
- return ((bitField0_ & 0x00000040) == 0x00000040);
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Avatar getAvatar() {
- if (avatarBuilder_ == null) {
- return avatar_;
- } else {
- return avatarBuilder_.getMessage();
- }
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public Builder setAvatar(org.thoughtcrime.securesms.backup.BackupProtos.Avatar value) {
- if (avatarBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- avatar_ = value;
- onChanged();
- } else {
- avatarBuilder_.setMessage(value);
- }
- bitField0_ |= 0x00000040;
- return this;
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public Builder setAvatar(
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar.Builder builderForValue) {
- if (avatarBuilder_ == null) {
- avatar_ = builderForValue.build();
- onChanged();
- } else {
- avatarBuilder_.setMessage(builderForValue.build());
- }
- bitField0_ |= 0x00000040;
- return this;
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public Builder mergeAvatar(org.thoughtcrime.securesms.backup.BackupProtos.Avatar value) {
- if (avatarBuilder_ == null) {
- if (((bitField0_ & 0x00000040) == 0x00000040) &&
- avatar_ != org.thoughtcrime.securesms.backup.BackupProtos.Avatar.getDefaultInstance()) {
- avatar_ =
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar.newBuilder(avatar_).mergeFrom(value).buildPartial();
- } else {
- avatar_ = value;
- }
- onChanged();
- } else {
- avatarBuilder_.mergeFrom(value);
- }
- bitField0_ |= 0x00000040;
- return this;
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public Builder clearAvatar() {
- if (avatarBuilder_ == null) {
- avatar_ = org.thoughtcrime.securesms.backup.BackupProtos.Avatar.getDefaultInstance();
- onChanged();
- } else {
- avatarBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000040);
- return this;
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Avatar.Builder getAvatarBuilder() {
- bitField0_ |= 0x00000040;
- onChanged();
- return getAvatarFieldBuilder().getBuilder();
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.AvatarOrBuilder getAvatarOrBuilder() {
- if (avatarBuilder_ != null) {
- return avatarBuilder_.getMessageOrBuilder();
- } else {
- return avatar_;
- }
- }
- /**
- * optional .signal.Avatar avatar = 7;
- */
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar, org.thoughtcrime.securesms.backup.BackupProtos.Avatar.Builder, org.thoughtcrime.securesms.backup.BackupProtos.AvatarOrBuilder>
- getAvatarFieldBuilder() {
- if (avatarBuilder_ == null) {
- avatarBuilder_ = new com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Avatar, org.thoughtcrime.securesms.backup.BackupProtos.Avatar.Builder, org.thoughtcrime.securesms.backup.BackupProtos.AvatarOrBuilder>(
- avatar_,
- getParentForChildren(),
- isClean());
- avatar_ = null;
- }
- return avatarBuilder_;
- }
-
- // optional .signal.Sticker sticker = 8;
- private org.thoughtcrime.securesms.backup.BackupProtos.Sticker sticker_ = org.thoughtcrime.securesms.backup.BackupProtos.Sticker.getDefaultInstance();
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker, org.thoughtcrime.securesms.backup.BackupProtos.Sticker.Builder, org.thoughtcrime.securesms.backup.BackupProtos.StickerOrBuilder> stickerBuilder_;
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public boolean hasSticker() {
- return ((bitField0_ & 0x00000080) == 0x00000080);
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Sticker getSticker() {
- if (stickerBuilder_ == null) {
- return sticker_;
- } else {
- return stickerBuilder_.getMessage();
- }
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public Builder setSticker(org.thoughtcrime.securesms.backup.BackupProtos.Sticker value) {
- if (stickerBuilder_ == null) {
- if (value == null) {
- throw new NullPointerException();
- }
- sticker_ = value;
- onChanged();
- } else {
- stickerBuilder_.setMessage(value);
- }
- bitField0_ |= 0x00000080;
- return this;
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public Builder setSticker(
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker.Builder builderForValue) {
- if (stickerBuilder_ == null) {
- sticker_ = builderForValue.build();
- onChanged();
- } else {
- stickerBuilder_.setMessage(builderForValue.build());
- }
- bitField0_ |= 0x00000080;
- return this;
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public Builder mergeSticker(org.thoughtcrime.securesms.backup.BackupProtos.Sticker value) {
- if (stickerBuilder_ == null) {
- if (((bitField0_ & 0x00000080) == 0x00000080) &&
- sticker_ != org.thoughtcrime.securesms.backup.BackupProtos.Sticker.getDefaultInstance()) {
- sticker_ =
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker.newBuilder(sticker_).mergeFrom(value).buildPartial();
- } else {
- sticker_ = value;
- }
- onChanged();
- } else {
- stickerBuilder_.mergeFrom(value);
- }
- bitField0_ |= 0x00000080;
- return this;
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public Builder clearSticker() {
- if (stickerBuilder_ == null) {
- sticker_ = org.thoughtcrime.securesms.backup.BackupProtos.Sticker.getDefaultInstance();
- onChanged();
- } else {
- stickerBuilder_.clear();
- }
- bitField0_ = (bitField0_ & ~0x00000080);
- return this;
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.Sticker.Builder getStickerBuilder() {
- bitField0_ |= 0x00000080;
- onChanged();
- return getStickerFieldBuilder().getBuilder();
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- public org.thoughtcrime.securesms.backup.BackupProtos.StickerOrBuilder getStickerOrBuilder() {
- if (stickerBuilder_ != null) {
- return stickerBuilder_.getMessageOrBuilder();
- } else {
- return sticker_;
- }
- }
- /**
- * optional .signal.Sticker sticker = 8;
- */
- private com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker, org.thoughtcrime.securesms.backup.BackupProtos.Sticker.Builder, org.thoughtcrime.securesms.backup.BackupProtos.StickerOrBuilder>
- getStickerFieldBuilder() {
- if (stickerBuilder_ == null) {
- stickerBuilder_ = new com.google.protobuf.SingleFieldBuilder<
- org.thoughtcrime.securesms.backup.BackupProtos.Sticker, org.thoughtcrime.securesms.backup.BackupProtos.Sticker.Builder, org.thoughtcrime.securesms.backup.BackupProtos.StickerOrBuilder>(
- sticker_,
- getParentForChildren(),
- isClean());
- sticker_ = null;
- }
- return stickerBuilder_;
- }
-
- // @@protoc_insertion_point(builder_scope:signal.BackupFrame)
- }
-
- static {
- defaultInstance = new BackupFrame(true);
- defaultInstance.initFields();
- }
-
- // @@protoc_insertion_point(class_scope:signal.BackupFrame)
- }
-
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_SqlStatement_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_SqlStatement_fieldAccessorTable;
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_SqlStatement_SqlParameter_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_SqlStatement_SqlParameter_fieldAccessorTable;
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_SharedPreference_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_SharedPreference_fieldAccessorTable;
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_Attachment_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_Attachment_fieldAccessorTable;
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_Sticker_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_Sticker_fieldAccessorTable;
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_Avatar_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_Avatar_fieldAccessorTable;
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_DatabaseVersion_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_DatabaseVersion_fieldAccessorTable;
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_Header_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_Header_fieldAccessorTable;
- private static com.google.protobuf.Descriptors.Descriptor
- internal_static_signal_BackupFrame_descriptor;
- private static
- com.google.protobuf.GeneratedMessage.FieldAccessorTable
- internal_static_signal_BackupFrame_fieldAccessorTable;
-
- public static com.google.protobuf.Descriptors.FileDescriptor
- getDescriptor() {
- return descriptor;
- }
- private static com.google.protobuf.Descriptors.FileDescriptor
- descriptor;
- static {
- java.lang.String[] descriptorData = {
- "\n\rBackups.proto\022\006signal\"\342\001\n\014SqlStatement" +
- "\022\021\n\tstatement\030\001 \001(\t\0225\n\nparameters\030\002 \003(\0132" +
- "!.signal.SqlStatement.SqlParameter\032\207\001\n\014S" +
- "qlParameter\022\026\n\016stringParamter\030\001 \001(\t\022\030\n\020i" +
- "ntegerParameter\030\002 \001(\004\022\027\n\017doubleParameter" +
- "\030\003 \001(\001\022\025\n\rblobParameter\030\004 \001(\014\022\025\n\rnullpar" +
- "ameter\030\005 \001(\010\"<\n\020SharedPreference\022\014\n\004file" +
- "\030\001 \001(\t\022\013\n\003key\030\002 \001(\t\022\r\n\005value\030\003 \001(\t\"A\n\nAt" +
- "tachment\022\r\n\005rowId\030\001 \001(\004\022\024\n\014attachmentId\030" +
- "\002 \001(\004\022\016\n\006length\030\003 \001(\r\"(\n\007Sticker\022\r\n\005rowI",
- "d\030\001 \001(\004\022\016\n\006length\030\002 \001(\r\"&\n\006Avatar\022\014\n\004nam" +
- "e\030\001 \001(\t\022\016\n\006length\030\002 \001(\r\"\"\n\017DatabaseVersi" +
- "on\022\017\n\007version\030\001 \001(\r\"\"\n\006Header\022\n\n\002iv\030\001 \001(" +
- "\014\022\014\n\004salt\030\002 \001(\014\"\245\002\n\013BackupFrame\022\036\n\006heade" +
- "r\030\001 \001(\0132\016.signal.Header\022\'\n\tstatement\030\002 \001" +
- "(\0132\024.signal.SqlStatement\022,\n\npreference\030\003" +
- " \001(\0132\030.signal.SharedPreference\022&\n\nattach" +
- "ment\030\004 \001(\0132\022.signal.Attachment\022(\n\007versio" +
- "n\030\005 \001(\0132\027.signal.DatabaseVersion\022\013\n\003end\030" +
- "\006 \001(\010\022\036\n\006avatar\030\007 \001(\0132\016.signal.Avatar\022 \n",
- "\007sticker\030\010 \001(\0132\017.signal.StickerB1\n!org.t" +
- "houghtcrime.securesms.backupB\014BackupProt" +
- "os"
- };
- com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
- new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
- public com.google.protobuf.ExtensionRegistry assignDescriptors(
- com.google.protobuf.Descriptors.FileDescriptor root) {
- descriptor = root;
- internal_static_signal_SqlStatement_descriptor =
- getDescriptor().getMessageTypes().get(0);
- internal_static_signal_SqlStatement_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_SqlStatement_descriptor,
- new java.lang.String[] { "Statement", "Parameters", });
- internal_static_signal_SqlStatement_SqlParameter_descriptor =
- internal_static_signal_SqlStatement_descriptor.getNestedTypes().get(0);
- internal_static_signal_SqlStatement_SqlParameter_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_SqlStatement_SqlParameter_descriptor,
- new java.lang.String[] { "StringParamter", "IntegerParameter", "DoubleParameter", "BlobParameter", "Nullparameter", });
- internal_static_signal_SharedPreference_descriptor =
- getDescriptor().getMessageTypes().get(1);
- internal_static_signal_SharedPreference_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_SharedPreference_descriptor,
- new java.lang.String[] { "File", "Key", "Value", });
- internal_static_signal_Attachment_descriptor =
- getDescriptor().getMessageTypes().get(2);
- internal_static_signal_Attachment_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_Attachment_descriptor,
- new java.lang.String[] { "RowId", "AttachmentId", "Length", });
- internal_static_signal_Sticker_descriptor =
- getDescriptor().getMessageTypes().get(3);
- internal_static_signal_Sticker_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_Sticker_descriptor,
- new java.lang.String[] { "RowId", "Length", });
- internal_static_signal_Avatar_descriptor =
- getDescriptor().getMessageTypes().get(4);
- internal_static_signal_Avatar_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_Avatar_descriptor,
- new java.lang.String[] { "Name", "Length", });
- internal_static_signal_DatabaseVersion_descriptor =
- getDescriptor().getMessageTypes().get(5);
- internal_static_signal_DatabaseVersion_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_DatabaseVersion_descriptor,
- new java.lang.String[] { "Version", });
- internal_static_signal_Header_descriptor =
- getDescriptor().getMessageTypes().get(6);
- internal_static_signal_Header_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_Header_descriptor,
- new java.lang.String[] { "Iv", "Salt", });
- internal_static_signal_BackupFrame_descriptor =
- getDescriptor().getMessageTypes().get(7);
- internal_static_signal_BackupFrame_fieldAccessorTable = new
- com.google.protobuf.GeneratedMessage.FieldAccessorTable(
- internal_static_signal_BackupFrame_descriptor,
- new java.lang.String[] { "Header", "Statement", "Preference", "Attachment", "Version", "End", "Avatar", "Sticker", });
- return null;
- }
- };
- com.google.protobuf.Descriptors.FileDescriptor
- .internalBuildGeneratedFileFrom(descriptorData,
- new com.google.protobuf.Descriptors.FileDescriptor[] {
- }, assigner);
- }
-
- // @@protoc_insertion_point(outer_class_scope)
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt
deleted file mode 100644
index a94c866c09..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-package org.thoughtcrime.securesms.backup
-
-import android.app.Activity
-import android.app.Application
-import android.content.Intent
-import android.graphics.Typeface
-import android.net.Uri
-import android.os.Bundle
-import android.provider.OpenableColumns
-import android.text.Spannable
-import android.text.SpannableStringBuilder
-import android.text.style.ClickableSpan
-import android.text.style.StyleSpan
-import android.view.View
-import android.widget.Toast
-import androidx.activity.result.ActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.viewModels
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.viewModelScope
-import com.google.android.gms.common.util.Strings
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import network.loki.messenger.R
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.utilities.Log
-import org.thoughtcrime.securesms.BaseActionBarActivity
-import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
-import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
-import org.thoughtcrime.securesms.database.DatabaseFactory
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent
-import org.thoughtcrime.securesms.home.HomeActivity
-import org.thoughtcrime.securesms.notifications.NotificationChannels
-import org.thoughtcrime.securesms.util.BackupUtil
-import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
-import org.thoughtcrime.securesms.util.show
-
-class BackupRestoreActivity : BaseActionBarActivity() {
-
- companion object {
- private const val TAG = "BackupRestoreActivity"
- }
-
- private val viewModel by viewModels()
-
- private val fileSelectionResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()
- ) { result: ActivityResult ->
- if (result.resultCode == Activity.RESULT_OK && result.data != null && result.data!!.data != null) {
- viewModel.backupFile.value = result.data!!.data!!
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setUpActionBarSessionLogo()
-
-// val viewBinding = DataBindingUtil.setContentView(this, R.layout.activity_backup_restore)
-// viewBinding.lifecycleOwner = this
-// viewBinding.viewModel = viewModel
-
-// viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() }
-
-// viewBinding.buttonSelectFile.setOnClickListener {
-// fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
-// //FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly
-// // and the backup files are unavailable for selection.
-//// type = BackupUtil.BACKUP_FILE_MIME_TYPE
-// type = "*/*"
-// })
-// }
-
-// viewBinding.backupCode.addTextChangedListener { text -> viewModel.backupPassphrase.value = text.toString() }
-
- // Focus passphrase text edit when backup file is selected.
-// viewModel.backupFile.observe(this, { backupFile ->
-// if (backupFile != null) viewBinding.backupCode.post {
-// viewBinding.backupCode.requestFocus()
-// (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
-// .showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT)
-// }
-// })
-
- // React to backup import result.
- viewModel.backupImportResult.observe(this) { result ->
- if (result != null) when (result) {
- BackupRestoreViewModel.BackupRestoreResult.SUCCESS -> {
- val intent = Intent(this, HomeActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- this.show(intent)
- }
- BackupRestoreViewModel.BackupRestoreResult.FAILURE_VERSION_DOWNGRADE ->
- Toast.makeText(this, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
- BackupRestoreViewModel.BackupRestoreResult.FAILURE_UNKNOWN ->
- Toast.makeText(this, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
- }
- }
-
- //region Legal info views
- val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
- termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- termsExplanation.setSpan(object : ClickableSpan() {
- override fun onClick(widget: View) {
- openURL("https://getsession.org/terms-of-service/")
- }
- }, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- termsExplanation.setSpan(object : ClickableSpan() {
- override fun onClick(widget: View) {
- openURL("https://getsession.org/privacy-policy/")
- }
- }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-// viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
-// viewBinding.termsTextView.text = termsExplanation
- //endregion
- }
-
- private fun openURL(url: String) {
- try {
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
- startActivity(intent)
- } catch (e: Exception) {
- Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
- }
- }
-}
-
-class BackupRestoreViewModel(application: Application): AndroidViewModel(application) {
-
- companion object {
- private const val TAG = "BackupRestoreViewModel"
-
- @JvmStatic
- fun uriToFileName(view: View, fileUri: Uri?): String? {
- fileUri ?: return null
-
- view.context.contentResolver.query(fileUri, null, null, null, null).use {
- val nameIndex = it!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- it.moveToFirst()
- return it.getString(nameIndex)
- }
- }
-
- @JvmStatic
- fun validateData(fileUri: Uri?, passphrase: String?): Boolean {
- return fileUri != null &&
- !Strings.isEmptyOrWhitespace(passphrase) &&
- passphrase!!.length == BackupUtil.BACKUP_PASSPHRASE_LENGTH
- }
- }
-
- val backupFile = MutableLiveData(null)
- val backupPassphrase = MutableLiveData(null)
-
- val processingBackupFile = MutableLiveData(false)
- val backupImportResult = MutableLiveData(null)
-
- fun tryRestoreBackup() = viewModelScope.launch {
- if (processingBackupFile.value == true) return@launch
- if (backupImportResult.value == BackupRestoreResult.SUCCESS) return@launch
- if (!validateData(backupFile.value, backupPassphrase.value)) return@launch
-
- val context = getApplication()
- val backupFile = backupFile.value!!
- val passphrase = backupPassphrase.value!!
-
- val result: BackupRestoreResult
-
- processingBackupFile.value = true
-
- withContext(Dispatchers.IO) {
- result = try {
- val database = DatabaseComponent.get(context).openHelper().readableDatabase
- FullBackupImporter.importFromUri(
- context,
- AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
- database,
- backupFile,
- passphrase
- )
- DatabaseFactory.upgradeRestored(context, database)
- NotificationChannels.restoreContactNotificationChannels(context)
- TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
- TextSecurePreferences.setHasViewedSeed(context, true)
- TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
-
- BackupRestoreResult.SUCCESS
- } catch (e: DatabaseDowngradeException) {
- Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
- BackupRestoreResult.FAILURE_VERSION_DOWNGRADE
- } catch (e: Exception) {
- Log.w(TAG, e)
- BackupRestoreResult.FAILURE_UNKNOWN
- }
- }
-
- processingBackupFile.value = false
-
- backupImportResult.value = result
- }
-
- enum class BackupRestoreResult {
- SUCCESS, FAILURE_VERSION_DOWNGRADE, FAILURE_UNKNOWN
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt
deleted file mode 100644
index 33b8b67258..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt
+++ /dev/null
@@ -1,447 +0,0 @@
-package org.thoughtcrime.securesms.backup
-
-import android.content.Context
-import android.database.Cursor
-import android.net.Uri
-import android.text.TextUtils
-import androidx.annotation.WorkerThread
-import com.annimon.stream.function.Consumer
-import com.annimon.stream.function.Predicate
-import com.google.protobuf.ByteString
-import net.sqlcipher.database.SQLiteDatabase
-import org.greenrobot.eventbus.EventBus
-import org.session.libsession.avatars.AvatarHelper
-import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
-import org.session.libsession.utilities.Conversions
-import org.session.libsession.utilities.Util
-import org.session.libsignal.crypto.kdf.HKDFv3
-import org.session.libsignal.utilities.ByteUtil
-import org.session.libsignal.utilities.Log
-import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
-import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
-import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
-import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
-import org.thoughtcrime.securesms.backup.BackupProtos.Header
-import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
-import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
-import org.thoughtcrime.securesms.backup.BackupProtos.Sticker
-import org.thoughtcrime.securesms.crypto.AttachmentSecret
-import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
-import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
-import org.thoughtcrime.securesms.database.AttachmentDatabase
-import org.thoughtcrime.securesms.database.GroupReceiptDatabase
-import org.thoughtcrime.securesms.database.JobDatabase
-import org.thoughtcrime.securesms.database.LokiAPIDatabase
-import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
-import org.thoughtcrime.securesms.database.MmsDatabase
-import org.thoughtcrime.securesms.database.MmsSmsColumns
-import org.thoughtcrime.securesms.database.PushDatabase
-import org.thoughtcrime.securesms.database.SearchDatabase
-import org.thoughtcrime.securesms.database.SmsDatabase
-import org.thoughtcrime.securesms.util.BackupUtil
-import java.io.Closeable
-import java.io.File
-import java.io.FileInputStream
-import java.io.Flushable
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-import java.security.InvalidAlgorithmParameterException
-import java.security.InvalidKeyException
-import java.security.NoSuchAlgorithmException
-import java.util.LinkedList
-import javax.crypto.BadPaddingException
-import javax.crypto.Cipher
-import javax.crypto.IllegalBlockSizeException
-import javax.crypto.Mac
-import javax.crypto.NoSuchPaddingException
-import javax.crypto.spec.IvParameterSpec
-import javax.crypto.spec.SecretKeySpec
-
-object FullBackupExporter {
- private val TAG = FullBackupExporter::class.java.simpleName
-
- @JvmStatic
- @WorkerThread
- @Throws(IOException::class)
- fun export(context: Context,
- attachmentSecret: AttachmentSecret,
- input: SQLiteDatabase,
- fileUri: Uri,
- passphrase: String) {
-
- val baseOutputStream = context.contentResolver.openOutputStream(fileUri)
- ?: throw IOException("Cannot open an output stream for the file URI: $fileUri")
-
- var count = 0
- try {
- BackupFrameOutputStream(baseOutputStream, passphrase).use { outputStream ->
- outputStream.writeDatabaseVersion(input.version)
- val tables = exportSchema(input, outputStream)
- for (table in tables) if (shouldExportTable(table)) {
- count = when (table) {
- SmsDatabase.TABLE_NAME, MmsDatabase.TABLE_NAME -> {
- exportTable(table, input, outputStream,
- { cursor: Cursor ->
- cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0
- },
- null,
- count)
- }
- GroupReceiptDatabase.TABLE_NAME -> {
- exportTable(table, input, outputStream,
- { cursor: Cursor ->
- isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID)))
- },
- null,
- count)
- }
- AttachmentDatabase.TABLE_NAME -> {
- exportTable(table, input, outputStream,
- { cursor: Cursor ->
- isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)))
- },
- { cursor: Cursor ->
- exportAttachment(attachmentSecret, cursor, outputStream)
- },
- count)
- }
- else -> {
- exportTable(table, input, outputStream, null, null, count)
- }
- }
- }
- for (preference in BackupUtil.getBackupRecords(context)) {
- EventBus.getDefault().post(BackupEvent.createProgress(++count))
- outputStream.writePreferenceEntry(preference)
- }
- for (preference in BackupPreferences.getBackupRecords(context)) {
- EventBus.getDefault().post(BackupEvent.createProgress(++count))
- outputStream.writePreferenceEntry(preference)
- }
- for (avatar in AvatarHelper.getAvatarFiles(context)) {
- EventBus.getDefault().post(BackupEvent.createProgress(++count))
- outputStream.writeAvatar(avatar.name, FileInputStream(avatar), avatar.length())
- }
- outputStream.writeEnd()
- }
- EventBus.getDefault().post(BackupEvent.createFinished())
- } catch (e: Exception) {
- Log.e(TAG, "Failed to make full backup.", e)
- EventBus.getDefault().post(BackupEvent.createFinished(e))
- throw e
- }
- }
-
- private inline fun shouldExportTable(table: String): Boolean {
- return table != PushDatabase.TABLE_NAME &&
-
- table != LokiBackupFilesDatabase.TABLE_NAME &&
- table != LokiAPIDatabase.openGroupProfilePictureTable &&
-
- table != JobDatabase.Jobs.TABLE_NAME &&
- table != JobDatabase.Constraints.TABLE_NAME &&
- table != JobDatabase.Dependencies.TABLE_NAME &&
-
- !table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
- !table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
- !table.startsWith("sqlite_")
- }
-
- @Throws(IOException::class)
- private fun exportSchema(input: SQLiteDatabase, outputStream: BackupFrameOutputStream): List {
- val tables: MutableList = LinkedList()
- input.rawQuery("SELECT sql, name, type FROM sqlite_master", null).use { cursor ->
- while (cursor != null && cursor.moveToNext()) {
- val sql = cursor.getString(0)
- val name = cursor.getString(1)
- val type = cursor.getString(2)
- if (sql != null) {
- val isSmsFtsSecretTable = name != null && name != SearchDatabase.SMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME)
- val isMmsFtsSecretTable = name != null && name != SearchDatabase.MMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME)
- if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
- if ("table" == type) {
- tables.add(name)
- }
- outputStream.writeSql(SqlStatement.newBuilder().setStatement(cursor.getString(0)).build())
- }
- }
- }
- }
- return tables
- }
-
- @Throws(IOException::class)
- private fun exportTable(table: String,
- input: SQLiteDatabase,
- outputStream: BackupFrameOutputStream,
- predicate: Predicate?,
- postProcess: Consumer?,
- count: Int): Int {
- var count = count
- val template = "INSERT INTO $table VALUES "
- input.rawQuery("SELECT * FROM $table", null).use { cursor ->
- while (cursor != null && cursor.moveToNext()) {
- EventBus.getDefault().post(BackupEvent.createProgress(++count))
- if (predicate != null && !predicate.test(cursor)) continue
-
- val statement = StringBuilder(template)
- val statementBuilder = SqlStatement.newBuilder()
- statement.append('(')
- for (i in 0 until cursor.columnCount) {
- statement.append('?')
- when (cursor.getType(i)) {
- Cursor.FIELD_TYPE_STRING -> {
- statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
- .setStringParamter(cursor.getString(i)))
- }
- Cursor.FIELD_TYPE_FLOAT -> {
- statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
- .setDoubleParameter(cursor.getDouble(i)))
- }
- Cursor.FIELD_TYPE_INTEGER -> {
- statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
- .setIntegerParameter(cursor.getLong(i)))
- }
- Cursor.FIELD_TYPE_BLOB -> {
- statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
- .setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))))
- }
- Cursor.FIELD_TYPE_NULL -> {
- statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
- .setNullparameter(true))
- }
- else -> {
- throw AssertionError("unknown type?" + cursor.getType(i))
- }
- }
- if (i < cursor.columnCount - 1) {
- statement.append(',')
- }
- }
- statement.append(')')
- outputStream.writeSql(statementBuilder.setStatement(statement.toString()).build())
- postProcess?.accept(cursor)
- }
- }
- return count
- }
-
- private fun exportAttachment(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
- try {
- val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID))
- val uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))
- var size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE))
- val data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA))
- val random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM))
- if (!TextUtils.isEmpty(data) && size <= 0) {
- size = calculateVeryOldStreamLength(attachmentSecret, random, data)
- }
- if (!TextUtils.isEmpty(data) && size > 0) {
- val inputStream: InputStream = if (random != null && random.size == 32) {
- ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
- } else {
- ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
- }
- outputStream.writeAttachment(AttachmentId(rowId, uniqueId), inputStream, size)
- }
- } catch (e: IOException) {
- Log.w(TAG, e)
- }
- }
-
- @Throws(IOException::class)
- private fun calculateVeryOldStreamLength(attachmentSecret: AttachmentSecret, random: ByteArray?, data: String): Long {
- var result: Long = 0
- val inputStream: InputStream = if (random != null && random.size == 32) {
- ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
- } else {
- ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
- }
- var read: Int
- val buffer = ByteArray(8192)
- while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) {
- result += read.toLong()
- }
- return result
- }
-
- private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
- val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
- val where = MmsSmsColumns.ID + " = ?"
- val args = arrayOf(mmsId.toString())
- db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
- if (mmsCursor != null && mmsCursor.moveToFirst()) {
- return mmsCursor.getLong(0) == 0L
- }
- }
- return false
- }
-
- private class BackupFrameOutputStream : Closeable, Flushable {
-
- private val outputStream: OutputStream
- private var cipher: Cipher
- private var mac: Mac
- private val cipherKey: ByteArray
- private val macKey: ByteArray
- private val iv: ByteArray
-
- private var counter: Int = 0
-
- constructor(outputStream: OutputStream, passphrase: String) : super() {
- try {
- val salt = Util.getSecretBytes(32)
- val key = BackupUtil.computeBackupKey(passphrase, salt)
- val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
- val split = ByteUtil.split(derived, 32, 32)
- cipherKey = split[0]
- macKey = split[1]
- cipher = Cipher.getInstance("AES/CTR/NoPadding")
- mac = Mac.getInstance("HmacSHA256")
- this.outputStream = outputStream
- iv = Util.getSecretBytes(16)
- counter = Conversions.byteArrayToInt(iv)
- mac.init(SecretKeySpec(macKey, "HmacSHA256"))
- val header = BackupFrame.newBuilder().setHeader(Header.newBuilder()
- .setIv(ByteString.copyFrom(iv))
- .setSalt(ByteString.copyFrom(salt)))
- .build().toByteArray()
- outputStream.write(Conversions.intToByteArray(header.size))
- outputStream.write(header)
- } catch (e: Exception) {
- when (e) {
- is NoSuchAlgorithmException,
- is NoSuchPaddingException,
- is InvalidKeyException -> {
- throw AssertionError(e)
- }
- else -> throw e
- }
- }
- }
-
- @Throws(IOException::class)
- fun writeSql(statement: SqlStatement) {
- write(outputStream, BackupFrame.newBuilder().setStatement(statement).build())
- }
-
- @Throws(IOException::class)
- fun writePreferenceEntry(preference: SharedPreference?) {
- write(outputStream, BackupFrame.newBuilder().setPreference(preference).build())
- }
-
- @Throws(IOException::class)
- fun writeAvatar(avatarName: String, inputStream: InputStream, size: Long) {
- write(outputStream, BackupFrame.newBuilder()
- .setAvatar(Avatar.newBuilder()
- .setName(avatarName)
- .setLength(Util.toIntExact(size))
- .build())
- .build())
- writeStream(inputStream)
- }
-
- @Throws(IOException::class)
- fun writeAttachment(attachmentId: AttachmentId, inputStream: InputStream, size: Long) {
- write(outputStream, BackupFrame.newBuilder()
- .setAttachment(Attachment.newBuilder()
- .setRowId(attachmentId.rowId)
- .setAttachmentId(attachmentId.uniqueId)
- .setLength(Util.toIntExact(size))
- .build())
- .build())
- writeStream(inputStream)
- }
-
- @Throws(IOException::class)
- fun writeSticker(rowId: Long, inputStream: InputStream, size: Long) {
- write(outputStream, BackupFrame.newBuilder()
- .setSticker(Sticker.newBuilder()
- .setRowId(rowId)
- .setLength(Util.toIntExact(size))
- .build())
- .build())
- writeStream(inputStream)
- }
-
- @Throws(IOException::class)
- fun writeDatabaseVersion(version: Int) {
- write(outputStream, BackupFrame.newBuilder()
- .setVersion(DatabaseVersion.newBuilder().setVersion(version))
- .build())
- }
-
- @Throws(IOException::class)
- fun writeEnd() {
- write(outputStream, BackupFrame.newBuilder().setEnd(true).build())
- }
-
- @Throws(IOException::class)
- private fun writeStream(inputStream: InputStream) {
- try {
- Conversions.intToByteArray(iv, 0, counter++)
- cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
- mac.update(iv)
- val buffer = ByteArray(8192)
- var read: Int
- while (inputStream.read(buffer).also { read = it } != -1) {
- val ciphertext = cipher.update(buffer, 0, read)
- if (ciphertext != null) {
- outputStream.write(ciphertext)
- mac.update(ciphertext)
- }
- }
- val remainder = cipher.doFinal()
- outputStream.write(remainder)
- mac.update(remainder)
- val attachmentDigest = mac.doFinal()
- outputStream.write(attachmentDigest, 0, 10)
- } catch (e: Exception) {
- when (e) {
- is InvalidKeyException,
- is InvalidAlgorithmParameterException,
- is IllegalBlockSizeException,
- is BadPaddingException -> {
- throw AssertionError(e)
- }
- else -> throw e
- }
- }
- }
-
- @Throws(IOException::class)
- private fun write(out: OutputStream, frame: BackupFrame) {
- try {
- Conversions.intToByteArray(iv, 0, counter++)
- cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
- val frameCiphertext = cipher.doFinal(frame.toByteArray())
- val frameMac = mac.doFinal(frameCiphertext)
- val length = Conversions.intToByteArray(frameCiphertext.size + 10)
- out.write(length)
- out.write(frameCiphertext)
- out.write(frameMac, 0, 10)
- } catch (e: Exception) {
- when (e) {
- is InvalidKeyException,
- is InvalidAlgorithmParameterException,
- is IllegalBlockSizeException,
- is BadPaddingException -> {
- throw AssertionError(e)
- }
- else -> throw e
- }
- }
- }
-
- @Throws(IOException::class)
- override fun flush() {
- outputStream.flush()
- }
-
- @Throws(IOException::class)
- override fun close() {
- outputStream.close()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt
deleted file mode 100644
index ba1df97d56..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt
+++ /dev/null
@@ -1,352 +0,0 @@
-package org.thoughtcrime.securesms.backup
-
-import android.annotation.SuppressLint
-import android.content.ContentValues
-import android.content.Context
-import android.net.Uri
-import androidx.annotation.WorkerThread
-import net.sqlcipher.database.SQLiteDatabase
-import org.greenrobot.eventbus.EventBus
-import org.session.libsession.avatars.AvatarHelper
-import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
-import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.Conversions
-import org.session.libsession.utilities.Util
-import org.session.libsignal.crypto.kdf.HKDFv3
-import org.session.libsignal.utilities.ByteUtil
-import org.session.libsignal.utilities.Log
-import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
-import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
-import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
-import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
-import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
-import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
-import org.thoughtcrime.securesms.crypto.AttachmentSecret
-import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
-import org.thoughtcrime.securesms.database.AttachmentDatabase
-import org.thoughtcrime.securesms.database.GroupReceiptDatabase
-import org.thoughtcrime.securesms.database.MmsDatabase
-import org.thoughtcrime.securesms.database.MmsSmsColumns
-import org.thoughtcrime.securesms.database.SearchDatabase
-import org.thoughtcrime.securesms.database.ThreadDatabase
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent
-import org.thoughtcrime.securesms.util.BackupUtil
-import java.io.Closeable
-import java.io.File
-import java.io.FileOutputStream
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-import java.security.InvalidAlgorithmParameterException
-import java.security.InvalidKeyException
-import java.security.MessageDigest
-import java.security.NoSuchAlgorithmException
-import java.util.LinkedList
-import java.util.Locale
-import javax.crypto.BadPaddingException
-import javax.crypto.Cipher
-import javax.crypto.IllegalBlockSizeException
-import javax.crypto.Mac
-import javax.crypto.NoSuchPaddingException
-import javax.crypto.spec.IvParameterSpec
-import javax.crypto.spec.SecretKeySpec
-
-object FullBackupImporter {
- /**
- * Because BackupProtos.SharedPreference was made only to serialize string values,
- * we use these 3-char prefixes to explicitly cast the values before inserting to a preference file.
- */
- const val PREF_PREFIX_TYPE_INT = "i__"
- const val PREF_PREFIX_TYPE_BOOLEAN = "b__"
-
- private val TAG = FullBackupImporter::class.java.simpleName
-
- @JvmStatic
- @WorkerThread
- @Throws(IOException::class)
- fun importFromUri(context: Context,
- attachmentSecret: AttachmentSecret,
- db: SQLiteDatabase,
- fileUri: Uri,
- passphrase: String) {
-
- val baseInputStream = context.contentResolver.openInputStream(fileUri)
- ?: throw IOException("Cannot open an input stream for the file URI: $fileUri")
-
- var count = 0
- try {
- BackupRecordInputStream(baseInputStream, passphrase).use { inputStream ->
- db.beginTransaction()
- dropAllTables(db)
- var frame: BackupFrame
- while (!inputStream.readFrame().also { frame = it }.end) {
- if (count++ % 100 == 0) EventBus.getDefault().post(BackupEvent.createProgress(count))
- when {
- frame.hasVersion() -> processVersion(db, frame.version)
- frame.hasStatement() -> processStatement(db, frame.statement)
- frame.hasPreference() -> processPreference(context, frame.preference)
- frame.hasAttachment() -> processAttachment(context, attachmentSecret, db, frame.attachment, inputStream)
- frame.hasAvatar() -> processAvatar(context, frame.avatar, inputStream)
- }
- }
- trimEntriesForExpiredMessages(context, db)
- db.setTransactionSuccessful()
- }
- } finally {
- if (db.inTransaction()) {
- db.endTransaction()
- }
- }
- EventBus.getDefault().post(BackupEvent.createFinished())
- }
-
- @Throws(IOException::class)
- private fun processVersion(db: SQLiteDatabase, version: DatabaseVersion) {
- if (version.version > db.version) {
- throw DatabaseDowngradeException(db.version, version.version)
- }
- db.version = version.version
- }
-
- private fun processStatement(db: SQLiteDatabase, statement: SqlStatement) {
- val isForSmsFtsSecretTable = statement.statement.contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_")
- val isForMmsFtsSecretTable = statement.statement.contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_")
- val isForSqliteSecretTable = statement.statement.toLowerCase(Locale.ENGLISH).startsWith("create table sqlite_")
- if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForSqliteSecretTable) {
- Log.i(TAG, "Ignoring import for statement: " + statement.statement)
- return
- }
- val parameters: MutableList = LinkedList()
- for (parameter in statement.parametersList) {
- when {
- parameter.hasStringParamter() -> parameters.add(parameter.stringParamter)
- parameter.hasDoubleParameter() -> parameters.add(parameter.doubleParameter)
- parameter.hasIntegerParameter() -> parameters.add(parameter.integerParameter)
- parameter.hasBlobParameter() -> parameters.add(parameter.blobParameter.toByteArray())
- parameter.hasNullparameter() -> parameters.add(null)
- }
- }
- if (parameters.size > 0) {
- db.execSQL(statement.statement, parameters.toTypedArray())
- } else {
- db.execSQL(statement.statement)
- }
- }
-
- @Throws(IOException::class)
- private fun processAttachment(context: Context, attachmentSecret: AttachmentSecret,
- db: SQLiteDatabase, attachment: Attachment,
- inputStream: BackupRecordInputStream) {
- val partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE)
- val dataFile = File.createTempFile("part", ".mms", partsDirectory)
- val output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false)
- inputStream.readAttachmentTo(output.second, attachment.length)
- val contentValues = ContentValues()
- contentValues.put(AttachmentDatabase.DATA, dataFile.absolutePath)
- contentValues.put(AttachmentDatabase.THUMBNAIL, null as String?)
- contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first)
- db.update(AttachmentDatabase.TABLE_NAME, contentValues,
- "${AttachmentDatabase.ROW_ID} = ? AND ${AttachmentDatabase.UNIQUE_ID} = ?",
- arrayOf(attachment.rowId.toString(), attachment.attachmentId.toString()))
- }
-
- @Throws(IOException::class)
- private fun processAvatar(context: Context, avatar: Avatar, inputStream: BackupRecordInputStream) {
- inputStream.readAttachmentTo(FileOutputStream(
- AvatarHelper.getAvatarFile(context, Address.fromExternal(context, avatar.name))), avatar.length)
- }
-
- @SuppressLint("ApplySharedPref")
- private fun processPreference(context: Context, preference: SharedPreference) {
- val preferences = context.getSharedPreferences(preference.file, 0)
- val key = preference.key
- val value = preference.value
-
- // See the comment next to PREF_PREFIX_TYPE_* constants.
- when {
- key.startsWith(PREF_PREFIX_TYPE_INT) ->
- preferences.edit().putInt(
- key.substring(PREF_PREFIX_TYPE_INT.length),
- value.toInt()
- ).commit()
- key.startsWith(PREF_PREFIX_TYPE_BOOLEAN) ->
- preferences.edit().putBoolean(
- key.substring(PREF_PREFIX_TYPE_BOOLEAN.length),
- value.toBoolean()
- ).commit()
- else ->
- preferences.edit().putString(key, value).commit()
- }
- }
-
- private fun dropAllTables(db: SQLiteDatabase) {
- db.rawQuery("SELECT name, type FROM sqlite_master", null).use { cursor ->
- while (cursor != null && cursor.moveToNext()) {
- val name = cursor.getString(0)
- val type = cursor.getString(1)
- if ("table" == type && !name.startsWith("sqlite_")) {
- db.execSQL("DROP TABLE IF EXISTS $name")
- }
- }
- }
- }
-
- private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
- val trimmedCondition = " NOT IN (SELECT ${MmsSmsColumns.ID} FROM ${MmsDatabase.TABLE_NAME})"
- db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
- val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
- val where = AttachmentDatabase.MMS_ID + trimmedCondition
- db.query(AttachmentDatabase.TABLE_NAME, columns, where, null, null, null, null).use { cursor ->
- while (cursor != null && cursor.moveToNext()) {
- DatabaseComponent.get(context).attachmentDatabase()
- .deleteAttachment(AttachmentId(cursor.getLong(0), cursor.getLong(1)))
- }
- }
- db.query(ThreadDatabase.TABLE_NAME, arrayOf(ThreadDatabase.ID),
- ThreadDatabase.EXPIRES_IN + " > 0", null, null, null, null).use { cursor ->
- while (cursor != null && cursor.moveToNext()) {
- DatabaseComponent.get(context).threadDatabase().update(cursor.getLong(0), false)
- }
- }
- }
-
- private class BackupRecordInputStream : Closeable {
- private val inputStream: InputStream
- private val cipher: Cipher
- private val mac: Mac
- private val cipherKey: ByteArray
- private val macKey: ByteArray
- private val iv: ByteArray
-
- private var counter = 0
-
- @Throws(IOException::class)
- constructor(inputStream: InputStream, passphrase: String) : super() {
- try {
- this.inputStream = inputStream
- val headerLengthBytes = ByteArray(4)
- Util.readFully(this.inputStream, headerLengthBytes)
- val headerLength = Conversions.byteArrayToInt(headerLengthBytes)
- val headerFrame = ByteArray(headerLength)
- Util.readFully(this.inputStream, headerFrame)
- val frame = BackupFrame.parseFrom(headerFrame)
- if (!frame.hasHeader()) {
- throw IOException("Backup stream does not start with header!")
- }
- val header = frame.header
- iv = header.iv.toByteArray()
- if (iv.size != 16) {
- throw IOException("Invalid IV length!")
- }
- val key = BackupUtil.computeBackupKey(passphrase, if (header.hasSalt()) header.salt.toByteArray() else null)
- val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
- val split = ByteUtil.split(derived, 32, 32)
- cipherKey = split[0]
- macKey = split[1]
- cipher = Cipher.getInstance("AES/CTR/NoPadding")
- mac = Mac.getInstance("HmacSHA256")
- mac.init(SecretKeySpec(macKey, "HmacSHA256"))
- counter = Conversions.byteArrayToInt(iv)
- } catch (e: Exception) {
- when (e) {
- is NoSuchAlgorithmException,
- is NoSuchPaddingException,
- is InvalidKeyException -> {
- throw AssertionError(e)
- }
- else -> throw e
- }
- }
- }
-
- @Throws(IOException::class)
- fun readFrame(): BackupFrame {
- return readFrame(inputStream)
- }
-
- @Throws(IOException::class)
- fun readAttachmentTo(out: OutputStream, length: Int) {
- var length = length
- try {
- Conversions.intToByteArray(iv, 0, counter++)
- cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
- mac.update(iv)
- val buffer = ByteArray(8192)
- while (length > 0) {
- val read = inputStream.read(buffer, 0, Math.min(buffer.size, length))
- if (read == -1) throw IOException("File ended early!")
- mac.update(buffer, 0, read)
- val plaintext = cipher.update(buffer, 0, read)
- if (plaintext != null) {
- out.write(plaintext, 0, plaintext.size)
- }
- length -= read
- }
- val plaintext = cipher.doFinal()
- if (plaintext != null) {
- out.write(plaintext, 0, plaintext.size)
- }
- out.close()
- val ourMac = ByteUtil.trim(mac.doFinal(), 10)
- val theirMac = ByteArray(10)
- try {
- Util.readFully(inputStream, theirMac)
- } catch (e: IOException) {
- throw IOException(e)
- }
- if (!MessageDigest.isEqual(ourMac, theirMac)) {
- throw IOException("Bad MAC")
- }
- } catch (e: Exception) {
- when (e) {
- is InvalidKeyException,
- is InvalidAlgorithmParameterException,
- is IllegalBlockSizeException,
- is BadPaddingException -> {
- throw AssertionError(e)
- }
- else -> throw e
- }
- }
- }
-
- @Throws(IOException::class)
- private fun readFrame(`in`: InputStream?): BackupFrame {
- return try {
- val length = ByteArray(4)
- Util.readFully(`in`, length)
- val frame = ByteArray(Conversions.byteArrayToInt(length))
- Util.readFully(`in`, frame)
- val theirMac = ByteArray(10)
- System.arraycopy(frame, frame.size - 10, theirMac, 0, theirMac.size)
- mac.update(frame, 0, frame.size - 10)
- val ourMac = ByteUtil.trim(mac.doFinal(), 10)
- if (!MessageDigest.isEqual(ourMac, theirMac)) {
- throw IOException("Bad MAC")
- }
- Conversions.intToByteArray(iv, 0, counter++)
- cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
- val plaintext = cipher.doFinal(frame, 0, frame.size - 10)
- BackupFrame.parseFrom(plaintext)
- } catch (e: Exception) {
- when (e) {
- is InvalidKeyException,
- is InvalidAlgorithmParameterException,
- is IllegalBlockSizeException,
- is BadPaddingException -> {
- throw AssertionError(e)
- }
- else -> throw e
- }
- }
- }
-
- @Throws(IOException::class)
- override fun close() {
- inputStream.close()
- }
- }
-
- class DatabaseDowngradeException internal constructor(currentVersion: Int, backupVersion: Int) :
- IOException("Tried to import a backup with version $backupVersion into a database with version $currentVersion")
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
index 7e732d1aa7..afa6944645 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
@@ -93,6 +93,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
super.onNewIntent(intent)
if (intent?.action == ACTION_ANSWER) {
val answerIntent = WebRtcCallService.acceptCallIntent(this)
+ answerIntent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
ContextCompat.startForegroundService(this, answerIntent)
}
}
@@ -106,6 +107,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
setShowWhenLocked(true)
setTurnScreenOn(true)
}
+
window.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
@@ -249,17 +251,12 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
viewModel.callState.collect { state ->
Log.d("Loki", "Consuming view model state $state")
when (state) {
- CALL_RINGING -> {
- if (wantsToAnswer) {
- answerCall()
- wantsToAnswer = false
- }
- }
- CALL_OUTGOING -> {
- }
- CALL_CONNECTED -> {
+ CALL_RINGING -> if (wantsToAnswer) {
+ answerCall()
wantsToAnswer = false
}
+ CALL_CONNECTED -> wantsToAnswer = false
+ else -> {}
}
updateControls(state)
}
@@ -339,6 +336,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
if (isEnabled) {
viewModel.localRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
+
+ // Mirror the video preview of the person making the call to prevent disorienting them
+ surfaceView.setMirror(true)
+
binding.localRenderer.addView(surfaceView)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java
deleted file mode 100644
index 195c066d45..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package org.thoughtcrime.securesms.components;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.AsyncTask;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.thoughtcrime.securesms.ApplicationContext;
-import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView;
-import org.thoughtcrime.securesms.database.model.MessageRecord;
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
-import org.thoughtcrime.securesms.service.ExpiringMessageManager;
-import org.thoughtcrime.securesms.util.DateUtils;
-
-import java.util.Locale;
-
-import network.loki.messenger.R;
-
-public class ConversationItemFooter extends LinearLayout {
-
- private TextView dateView;
- private ExpirationTimerView timerView;
- private ImageView insecureIndicatorView;
- private DeliveryStatusView deliveryStatusView;
-
- public ConversationItemFooter(Context context) {
- super(context);
- init(null);
- }
-
- public ConversationItemFooter(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- init(attrs);
- }
-
- public ConversationItemFooter(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init(attrs);
- }
-
- private void init(@Nullable AttributeSet attrs) {
- inflate(getContext(), R.layout.conversation_item_footer, this);
-
- dateView = findViewById(R.id.footer_date);
- timerView = findViewById(R.id.footer_expiration_timer);
- insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
- deliveryStatusView = findViewById(R.id.footer_delivery_status);
-
- if (attrs != null) {
- TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
- setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white)));
- setIconColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_icon_color, getResources().getColor(R.color.core_white)));
- typedArray.recycle();
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- timerView.stopAnimation();
- }
-
- public void setMessageRecord(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
- presentDate(messageRecord, locale);
- presentTimer(messageRecord);
- presentInsecureIndicator(messageRecord);
- presentDeliveryStatus(messageRecord);
- }
-
- public void setTextColor(int color) {
- dateView.setTextColor(color);
- }
-
- public void setIconColor(int color) {
- timerView.setColorFilter(color);
- insecureIndicatorView.setColorFilter(color);
- deliveryStatusView.setTint(color);
- }
-
- private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
- dateView.forceLayout();
-
- if (messageRecord.isFailed()) {
- dateView.setText(R.string.ConversationItem_error_not_delivered);
- } else {
- dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
- }
- }
-
- @SuppressLint("StaticFieldLeak")
- private void presentTimer(@NonNull final MessageRecord messageRecord) {
- if (messageRecord.getExpiresIn() > 0 && !messageRecord.isPending()) {
- this.timerView.setVisibility(View.VISIBLE);
- this.timerView.setPercentComplete(0);
-
- if (messageRecord.getExpireStarted() > 0) {
- this.timerView.setExpirationTime(messageRecord.getExpireStarted(),
- messageRecord.getExpiresIn());
- this.timerView.startAnimation();
-
- if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= System.currentTimeMillis()) {
- ApplicationContext.getInstance(getContext()).getExpiringMessageManager().checkSchedule();
- }
- } else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- ExpiringMessageManager expirationManager = ApplicationContext.getInstance(getContext()).getExpiringMessageManager();
- long id = messageRecord.getId();
- boolean mms = messageRecord.isMms();
-
- if (mms) DatabaseComponent.get(getContext()).mmsDatabase().markExpireStarted(id);
- else DatabaseComponent.get(getContext()).smsDatabase().markExpireStarted(id);
-
- expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- } else {
- this.timerView.setVisibility(View.GONE);
- }
- }
-
- private void presentInsecureIndicator(@NonNull MessageRecord messageRecord) {
- insecureIndicatorView.setVisibility(View.GONE);
- }
-
- private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
- if (!messageRecord.isFailed()) {
- if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
- else if (messageRecord.isPending()) deliveryStatusView.setPending();
- else if (messageRecord.isRead()) deliveryStatusView.setRead();
- else if (messageRecord.isDelivered()) deliveryStatusView.setDelivered();
- else deliveryStatusView.setSent();
- } else {
- deliveryStatusView.setNone();
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/GlideBitmapListeningTarget.java b/app/src/main/java/org/thoughtcrime/securesms/components/GlideBitmapListeningTarget.java
index 61094fb7df..157bc215e6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/GlideBitmapListeningTarget.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/GlideBitmapListeningTarget.java
@@ -4,30 +4,48 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+
+import android.view.View;
import android.widget.ImageView;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import org.session.libsignal.utilities.SettableFuture;
+import java.lang.ref.WeakReference;
+
public class GlideBitmapListeningTarget extends BitmapImageViewTarget {
private final SettableFuture loaded;
+ private final WeakReference loadingView;
- public GlideBitmapListeningTarget(@NonNull ImageView view, @NonNull SettableFuture loaded) {
+ public GlideBitmapListeningTarget(@NonNull ImageView view, @Nullable View loadingView, @NonNull SettableFuture loaded) {
super(view);
this.loaded = loaded;
+ this.loadingView = new WeakReference(loadingView);
}
@Override
protected void setResource(@Nullable Bitmap resource) {
super.setResource(resource);
loaded.set(true);
+
+ View loadingViewInstance = loadingView.get();
+
+ if (loadingViewInstance != null) {
+ loadingViewInstance.setVisibility(View.GONE);
+ }
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
loaded.set(true);
+
+ View loadingViewInstance = loadingView.get();
+
+ if (loadingViewInstance != null) {
+ loadingViewInstance.setVisibility(View.GONE);
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/GlideDrawableListeningTarget.java b/app/src/main/java/org/thoughtcrime/securesms/components/GlideDrawableListeningTarget.java
index d177900124..406c878ec9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/GlideDrawableListeningTarget.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/GlideDrawableListeningTarget.java
@@ -3,30 +3,48 @@ package org.thoughtcrime.securesms.components;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+
+import android.view.View;
import android.widget.ImageView;
import com.bumptech.glide.request.target.DrawableImageViewTarget;
import org.session.libsignal.utilities.SettableFuture;
+import java.lang.ref.WeakReference;
+
public class GlideDrawableListeningTarget extends DrawableImageViewTarget {
private final SettableFuture loaded;
+ private final WeakReference loadingView;
- public GlideDrawableListeningTarget(@NonNull ImageView view, @NonNull SettableFuture loaded) {
+ public GlideDrawableListeningTarget(@NonNull ImageView view, @Nullable View loadingView, @NonNull SettableFuture loaded) {
super(view);
this.loaded = loaded;
+ this.loadingView = new WeakReference(loadingView);
}
@Override
protected void setResource(@Nullable Drawable resource) {
super.setResource(resource);
loaded.set(true);
+
+ View loadingViewInstance = loadingView.get();
+
+ if (loadingViewInstance != null) {
+ loadingViewInstance.setVisibility(View.GONE);
+ }
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
loaded.set(true);
+
+ View loadingViewInstance = loadingView.get();
+
+ if (loadingViewInstance != null) {
+ loadingViewInstance.setVisibility(View.GONE);
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt
deleted file mode 100644
index df36719db2..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package org.thoughtcrime.securesms.components
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Paint
-import android.graphics.Path
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.RelativeLayout
-import network.loki.messenger.R
-import network.loki.messenger.databinding.ViewSeparatorBinding
-import org.thoughtcrime.securesms.util.toPx
-import org.session.libsession.utilities.ThemeUtil
-
-class LabeledSeparatorView : RelativeLayout {
-
- private lateinit var binding: ViewSeparatorBinding
- private val path = Path()
-
- private val paint: Paint by lazy {
- val result = Paint()
- result.style = Paint.Style.STROKE
- result.color = ThemeUtil.getThemedColor(context, R.attr.dividerHorizontal)
- result.strokeWidth = toPx(1, resources).toFloat()
- result.isAntiAlias = true
- result
- }
-
- // region Lifecycle
- constructor(context: Context) : super(context) {
- setUpViewHierarchy()
- }
-
- constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
- setUpViewHierarchy()
- }
-
- constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
- setUpViewHierarchy()
- }
-
- constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
- setUpViewHierarchy()
- }
-
- private fun setUpViewHierarchy() {
- binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
- val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
- addView(binding.root, layoutParams)
- setWillNotDraw(false)
- }
- // endregion
-
- // region Updating
- override fun onDraw(c: Canvas) {
- super.onDraw(c)
- val w = width.toFloat()
- val h = height.toFloat()
- val hMargin = toPx(16, resources).toFloat()
- path.reset()
- path.moveTo(0.0f, h / 2)
- path.lineTo(binding.titleTextView.left - hMargin, h / 2)
- path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
- path.moveTo(binding.titleTextView.right + hMargin, h / 2)
- path.lineTo(w, h / 2)
- path.close()
- c.drawPath(path, paint)
- }
- // endregion
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java b/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java
deleted file mode 100644
index 5b2199896a..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package org.thoughtcrime.securesms.components;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import org.thoughtcrime.securesms.mms.GlideRequests;
-
-import org.thoughtcrime.securesms.mms.ImageSlide;
-import org.thoughtcrime.securesms.mms.SlidesClickedListener;
-
-import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
-
-import network.loki.messenger.R;
-import okhttp3.HttpUrl;
-
-public class LinkPreviewView extends FrameLayout {
-
- private static final int TYPE_CONVERSATION = 0;
- private static final int TYPE_COMPOSE = 1;
-
- private ViewGroup container;
- private OutlinedThumbnailView thumbnail;
- private TextView title;
- private TextView site;
- private View divider;
- private View closeButton;
- private View spinner;
-
- private int type;
- private int defaultRadius;
- private CornerMask cornerMask;
- private Outliner outliner;
- private CloseClickedListener closeClickedListener;
-
- public LinkPreviewView(Context context) {
- super(context);
- init(null);
- }
-
- public LinkPreviewView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- init(attrs);
- }
-
- private void init(@Nullable AttributeSet attrs) {
- inflate(getContext(), R.layout.link_preview, this);
-
- container = findViewById(R.id.linkpreview_container);
- thumbnail = findViewById(R.id.linkpreview_thumbnail);
- title = findViewById(R.id.linkpreview_title);
- site = findViewById(R.id.linkpreview_site);
- divider = findViewById(R.id.linkpreview_divider);
- spinner = findViewById(R.id.linkpreview_progress_wheel);
- closeButton = findViewById(R.id.linkpreview_close);
- defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
- cornerMask = new CornerMask(this);
- outliner = new Outliner();
-
- outliner.setColor(getResources().getColor(R.color.transparent));
-
- if (attrs != null) {
- TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
- type = typedArray.getInt(R.styleable.LinkPreviewView_linkpreview_type, 0);
- typedArray.recycle();
- }
-
- if (type == TYPE_COMPOSE) {
- container.setBackgroundColor(Color.TRANSPARENT);
- container.setPadding(0, 0, 0, 0);
- divider.setVisibility(VISIBLE);
-
- closeButton.setOnClickListener(v -> {
- if (closeClickedListener != null) {
- closeClickedListener.onCloseClicked();
- }
- });
- }
-
- setWillNotDraw(false);
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
- if (type == TYPE_COMPOSE) return;
-
- cornerMask.mask(canvas);
- outliner.draw(canvas);
- }
-
- public void setLoading() {
- title.setVisibility(GONE);
- site.setVisibility(GONE);
- thumbnail.setVisibility(GONE);
- spinner.setVisibility(VISIBLE);
- closeButton.setVisibility(GONE);
- }
-
- public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) {
- setLinkPreview(glideRequests, linkPreview, showThumbnail);
- if (showCloseButton) {
- closeButton.setVisibility(VISIBLE);
- } else {
- closeButton.setVisibility(GONE);
- }
- }
-
- public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
- title.setVisibility(VISIBLE);
- site.setVisibility(VISIBLE);
- thumbnail.setVisibility(VISIBLE);
- spinner.setVisibility(GONE);
- closeButton.setVisibility(VISIBLE);
-
- title.setText(linkPreview.getTitle());
-
- HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
- if (url != null) {
- site.setText(url.topPrivateDomain());
- }
-
- if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
- thumbnail.setVisibility(VISIBLE);
- thumbnail.setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
- thumbnail.showDownloadText(false);
- } else {
- thumbnail.setVisibility(GONE);
- }
- }
-
- public void setCorners(int topLeft, int topRight) {
- cornerMask.setRadii(topLeft, topRight, 0, 0);
- outliner.setRadii(topLeft, topRight, 0, 0);
- thumbnail.setCorners(topLeft, defaultRadius, defaultRadius, defaultRadius);
- postInvalidate();
- }
-
- public void setCloseClickedListener(@Nullable CloseClickedListener closeClickedListener) {
- this.closeClickedListener = closeClickedListener;
- }
-
- public void setDownloadClickedListener(SlidesClickedListener listener) {
- thumbnail.setDownloadClickListener(listener);
- }
-
- public interface CloseClickedListener {
- void onCloseClicked();
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java
deleted file mode 100644
index 71bf8a2804..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.thoughtcrime.securesms.components;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-
-import org.session.libsession.utilities.ThemeUtil;
-import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
-
-import network.loki.messenger.R;
-
-public class OutlinedThumbnailView extends ThumbnailView {
-
- private CornerMask cornerMask;
- private Outliner outliner;
-
- public OutlinedThumbnailView(Context context) {
- super(context);
- init();
- }
-
- public OutlinedThumbnailView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- private void init() {
- cornerMask = new CornerMask(this);
- outliner = new Outliner();
-
- outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
- setWillNotDraw(false);
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
-
- cornerMask.mask(canvas);
- outliner.draw(canvas);
- }
-
- public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
- cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
- outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
- postInvalidate();
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java b/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java
deleted file mode 100644
index cb6cfc7abf..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.thoughtcrime.securesms.components;
-
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.RectF;
-
-import androidx.annotation.ColorInt;
-
-public class Outliner {
-
- private final float[] radii = new float[8];
- private final Path corners = new Path();
- private final RectF bounds = new RectF();
- private final Paint outlinePaint = new Paint();
- {
- outlinePaint.setStyle(Paint.Style.STROKE);
- outlinePaint.setStrokeWidth(1f);
- outlinePaint.setAntiAlias(true);
- }
-
- public void setColor(@ColorInt int color) {
- outlinePaint.setColor(color);
- }
-
- public void draw(Canvas canvas) {
- final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
-
- bounds.left = halfStrokeWidth;
- bounds.top = halfStrokeWidth;
- bounds.right = canvas.getWidth() - halfStrokeWidth;
- bounds.bottom = canvas.getHeight() - halfStrokeWidth;
-
- corners.reset();
- corners.addRoundRect(bounds, radii, Path.Direction.CW);
-
- canvas.drawPath(corners, outlinePaint);
- }
-
- public void setRadius(int radius) {
- setRadii(radius, radius, radius, radius);
- }
-
- public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) {
- radii[0] = radii[1] = topLeft;
- radii[2] = radii[3] = topRight;
- radii[4] = radii[5] = bottomRight;
- radii[6] = radii[7] = bottomLeft;
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
index 0ded9f346e..52e2d52ab1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
@@ -2,10 +2,10 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
+import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
-import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
@@ -18,48 +18,60 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
class ProfilePictureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {
- private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
- lateinit var glide: GlideRequests
+ private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
+ private val glide: GlideRequests = GlideApp.with(this)
var publicKey: String? = null
var displayName: String? = null
var additionalPublicKey: String? = null
var additionalDisplayName: String? = null
var isLarge = false
- private val profilePicturesCache = mutableMapOf()
- private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
- .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
+ private val profilePicturesCache = mutableMapOf()
+ private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
+ .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
+ private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
+ .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
// endregion
+ constructor(context: Context, sender: Recipient): this(context) {
+ update(sender)
+ }
+
// region Updating
fun update(recipient: Recipient) {
fun getUserDisplayName(publicKey: String): String {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
- fun isOpenGroupWithProfilePicture(recipient: Recipient): Boolean {
- return recipient.isOpenGroupRecipient && recipient.groupAvatarId != null
- }
- if (recipient.isGroupRecipient && !isOpenGroupWithProfilePicture(recipient)) {
+
+ if (recipient.isClosedGroupRecipient) {
val members = DatabaseComponent.get(context).groupDatabase()
.getGroupMemberAddresses(recipient.address.toGroupString(), true)
.sorted()
.take(2)
.toMutableList()
- val pk = members.getOrNull(0)?.serialize() ?: ""
- publicKey = pk
- displayName = getUserDisplayName(pk)
- val apk = members.getOrNull(1)?.serialize() ?: ""
- additionalPublicKey = apk
- additionalDisplayName = getUserDisplayName(apk)
+ if (members.size <= 1) {
+ publicKey = ""
+ displayName = ""
+ additionalPublicKey = ""
+ additionalDisplayName = ""
+ } else {
+ val pk = members.getOrNull(0)?.serialize() ?: ""
+ publicKey = pk
+ displayName = getUserDisplayName(pk)
+ val apk = members.getOrNull(1)?.serialize() ?: ""
+ additionalPublicKey = apk
+ additionalDisplayName = getUserDisplayName(apk)
+ }
} else if(recipient.isOpenGroupInboxRecipient) {
- val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize())
+ val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
this.publicKey = publicKey
displayName = getUserDisplayName(publicKey)
additionalPublicKey = null
@@ -73,12 +85,11 @@ class ProfilePictureView @JvmOverloads constructor(
}
fun update() {
- if (!this::glide.isInitialized) return
val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey
if (additionalPublicKey != null) {
- setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
- setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
+ setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName)
+ setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName)
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
} else {
glide.clear(binding.doubleModeImageView1)
@@ -86,14 +97,14 @@ class ProfilePictureView @JvmOverloads constructor(
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
}
if (additionalPublicKey == null && !isLarge) {
- setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
+ setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName)
binding.singleModeImageView.visibility = View.VISIBLE
} else {
glide.clear(binding.singleModeImageView)
binding.singleModeImageView.visibility = View.INVISIBLE
}
if (additionalPublicKey == null && isLarge) {
- setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
+ setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName)
binding.largeSingleModeImageView.visibility = View.VISIBLE
} else {
glide.clear(binding.largeSingleModeImageView)
@@ -101,32 +112,43 @@ class ProfilePictureView @JvmOverloads constructor(
}
}
- private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
+ private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {
if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
- if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
+ if (profilePicturesCache[imageView] == recipient) return
+ profilePicturesCache[imageView] = recipient
val signalProfilePicture = recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
- val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
+
+ glide.clear(imageView)
+
+ val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
+
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
- glide.clear(imageView)
glide.load(signalProfilePicture)
.placeholder(unknownRecipientDrawable)
.centerCrop()
- .error(unknownRecipientDrawable)
+ .error(glide.load(placeholder))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imageView)
- } else {
+ } else if (recipient.isCommunityRecipient && recipient.groupAvatarId == null) {
glide.clear(imageView)
+ glide.load(unknownOpenGroupDrawable)
+ .centerCrop()
+ .circleCrop()
+ .into(imageView)
+ } else {
glide.load(placeholder)
.placeholder(unknownRecipientDrawable)
.centerCrop()
+ .circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
}
- profilePicturesCache[publicKey] = recipient.profileAvatar
} else {
- imageView.setImageDrawable(null)
+ glide.load(unknownRecipientDrawable)
+ .centerCrop()
+ .into(imageView)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SafeViewPager.kt b/app/src/main/java/org/thoughtcrime/securesms/components/SafeViewPager.kt
new file mode 100644
index 0000000000..6748478736
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/SafeViewPager.kt
@@ -0,0 +1,30 @@
+package org.thoughtcrime.securesms.components
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import androidx.viewpager.widget.ViewPager
+
+/**
+ * An extension of ViewPager to swallow erroneous multi-touch exceptions.
+ *
+ * @see https://stackoverflow.com/questions/6919292/pointerindex-out-of-range-android-multitouch
+ */
+class SafeViewPager @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : ViewPager(context, attrs) {
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent?): Boolean = try {
+ super.onTouchEvent(event)
+ } catch (e: IllegalArgumentException) {
+ false
+ }
+
+ override fun onInterceptTouchEvent(event: MotionEvent?): Boolean = try {
+ super.onInterceptTouchEvent(event)
+ } catch (e: IllegalArgumentException) {
+ false
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java b/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java
index 8a56acd658..9032b26a2b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components;
-
import android.animation.Animator;
import android.content.Context;
import android.os.Build;
@@ -68,9 +67,7 @@ public class SearchToolbar extends LinearLayout {
}
@Override
- public boolean onQueryTextChange(String newText) {
- return onQueryTextSubmit(newText);
- }
+ public boolean onQueryTextChange(String newText) { return onQueryTextSubmit(newText); }
});
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java b/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java
index 6214c58531..98a623eef3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java
@@ -52,19 +52,4 @@ public class StickerView extends FrameLayout {
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
image.setOnLongClickListener(l);
}
-
- public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
- boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
-
- image.setImageResource(glideRequests, stickerSlide, showControls, false);
- missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
- }
-
- public void setThumbnailClickListener(@NonNull SlideClickListener listener) {
- image.setThumbnailClickListener(listener);
- }
-
- public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
- image.setDownloadClickListener(listener);
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
index d512e0924c..4f0072cc24 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
@@ -24,7 +24,7 @@ public class EmojiTextView extends AppCompatTextView {
private static final char ELLIPSIS = '…';
private CharSequence previousText;
- private BufferType previousBufferType;
+ private BufferType previousBufferType = BufferType.NORMAL;
private float originalFontSize;
private boolean useSystemEmoji;
private boolean sizeChangeInProgress;
@@ -49,6 +49,15 @@ public class EmojiTextView extends AppCompatTextView {
}
@Override public void setText(@Nullable CharSequence text, BufferType type) {
+ // No need to do anything special if the text is null or empty
+ if (text == null || text.length() == 0) {
+ previousText = text;
+ previousOverflowText = overflowText;
+ previousBufferType = type;
+ super.setText(text, type);
+ return;
+ }
+
EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text);
if (scaleEmojis && candidates != null && candidates.allEmojis) {
@@ -149,10 +158,15 @@ public class EmojiTextView extends AppCompatTextView {
}
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
- return Util.equals(previousText, text) &&
- Util.equals(previousOverflowText, overflowText) &&
- Util.equals(previousBufferType, bufferType) &&
- useSystemEmoji == useSystemEmoji() &&
+ CharSequence finalPrevText = (previousText == null || previousText.length() == 0 ? "" : previousText);
+ CharSequence finalText = (text == null || text.length() == 0 ? "" : text);
+ CharSequence finalPrevOverflowText = (previousOverflowText == null || previousOverflowText.length() == 0 ? "" : previousOverflowText);
+ CharSequence finalOverflowText = (overflowText == null || overflowText.length() == 0 ? "" : overflowText);
+
+ return Util.equals(finalPrevText, finalText) &&
+ Util.equals(finalPrevOverflowText, finalOverflowText) &&
+ Util.equals(previousBufferType, bufferType) &&
+ useSystemEmoji == useSystemEmoji() &&
!sizeChangeInProgress;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
index 6cc39c8d14..0e2de9068c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
@@ -3,133 +3,95 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
-import android.os.AsyncTask;
import android.preference.PreferenceManager;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
-import com.fasterxml.jackson.databind.type.CollectionType;
-import com.fasterxml.jackson.databind.type.TypeFactory;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
+import java.util.LinkedList;
import java.util.List;
import network.loki.messenger.R;
public class RecentEmojiPageModel implements EmojiPageModel {
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
- private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji2";
- private static final int EMOJI_LRU_SIZE = 50;
- public static final String KEY = "Recents";
- public static final List DEFAULT_REACTIONS_LIST =
- Arrays.asList("\ud83d\ude02", "\ud83e\udd70", "\ud83d\ude22", "\ud83d\ude21", "\ud83d\ude2e", "\ud83d\ude08");
+ public static final String RECENT_EMOJIS_KEY = "Recents";
- private final SharedPreferences prefs;
- private final LinkedHashSet recentlyUsed;
+ public static final LinkedList DEFAULT_REACTION_EMOJIS_LIST = new LinkedList<>(Arrays.asList(
+ "\ud83d\ude02",
+ "\ud83e\udd70",
+ "\ud83d\ude22",
+ "\ud83d\ude21",
+ "\ud83d\ude2e",
+ "\ud83d\ude08"));
+
+ public static final String DEFAULT_REACTION_EMOJIS_JSON_STRING = JsonUtil.toJson(new LinkedList<>(DEFAULT_REACTION_EMOJIS_LIST));
+ private static SharedPreferences prefs;
+ private static LinkedList recentlyUsed;
public RecentEmojiPageModel(Context context) {
- this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
- this.recentlyUsed = getPersistedCache();
- }
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
- private LinkedHashSet getPersistedCache() {
- String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]");
- try {
- CollectionType collectionType = TypeFactory.defaultInstance()
- .constructCollectionType(LinkedHashSet.class, String.class);
- return JsonUtil.getMapper().readValue(serialized, collectionType);
- } catch (IOException e) {
- Log.w(TAG, e);
- return new LinkedHashSet<>();
- }
+ // Note: Do NOT try to populate or update the persisted recent emojis in the constructor - the
+ // `getEmoji` method ends up getting called half-way through in a race-condition manner.
}
@Override
- public String getKey() {
- return KEY;
- }
+ public String getKey() { return RECENT_EMOJIS_KEY; }
- @Override public int getIconAttr() {
- return R.attr.emoji_category_recent;
- }
+ @Override public int getIconAttr() { return R.attr.emoji_category_recent; }
@Override public List getEmoji() {
- List recent = new ArrayList<>(recentlyUsed);
- List out = new ArrayList<>(DEFAULT_REACTIONS_LIST.size());
-
- for (int i = 0; i < DEFAULT_REACTIONS_LIST.size(); i++) {
- if (recent.size() > i) {
- out.add(recent.get(i));
- } else {
- out.add(DEFAULT_REACTIONS_LIST.get(i));
+ // Populate our recently used list if required (i.e., on first run)
+ if (recentlyUsed == null) {
+ try {
+ String recentlyUsedEmjoiJsonString = prefs.getString(RECENT_EMOJIS_KEY, DEFAULT_REACTION_EMOJIS_JSON_STRING);
+ recentlyUsed = JsonUtil.fromJson(recentlyUsedEmjoiJsonString, LinkedList.class);
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ Log.d(TAG, "Default reaction emoji data was corrupt (likely via key re-use on app upgrade) - rewriting fresh data.");
+ boolean writeSuccess = prefs.edit().putString(RECENT_EMOJIS_KEY, DEFAULT_REACTION_EMOJIS_JSON_STRING).commit();
+ if (!writeSuccess) { Log.w(TAG, "Failed to update recently used emojis in shared prefs."); }
+ recentlyUsed = DEFAULT_REACTION_EMOJIS_LIST;
}
}
-
- return out;
+ return new ArrayList<>(recentlyUsed);
}
@Override public List getDisplayEmoji() {
return Stream.of(getEmoji()).map(Emoji::new).toList();
}
- @Override public boolean hasSpriteMap() {
- return false;
- }
+ @Override public boolean hasSpriteMap() { return false; }
@Nullable
@Override
- public Uri getSpriteUri() {
- return null;
- }
+ public Uri getSpriteUri() { return null; }
- @Override public boolean isDynamic() {
- return true;
- }
+ @Override public boolean isDynamic() { return true; }
- public void onCodePointSelected(String emoji) {
- recentlyUsed.remove(emoji);
- recentlyUsed.add(emoji);
+ public static void onCodePointSelected(String emoji) {
+ // If the emoji is already in the recently used list then remove it..
+ if (recentlyUsed.contains(emoji)) { recentlyUsed.removeFirstOccurrence(emoji); }
- if (recentlyUsed.size() > EMOJI_LRU_SIZE) {
- Iterator iterator = recentlyUsed.iterator();
- iterator.next();
- iterator.remove();
- }
+ // ..and then regardless of whether the emoji used was already in the recently used list or not
+ // it gets placed as the first element in the list..
+ recentlyUsed.addFirst(emoji);
- final LinkedHashSet latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
- new AsyncTask() {
+ // Ensure that we only ever store data for a maximum of 6 recently used emojis (this code will
+ // execute if if we did NOT remove any occurrence of a previously used emoji but then added the
+ // new emoji to the front of the list).
+ while (recentlyUsed.size() > 6) { recentlyUsed.removeLast(); }
- @Override
- protected Void doInBackground(Void... params) {
- try {
- String serialized = JsonUtil.toJsonThrows(latestRecentlyUsed);
- prefs.edit()
- .putString(EMOJI_LRU_PREFERENCE, serialized)
- .apply();
- } catch (IOException e) {
- Log.w(TAG, e);
- }
-
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private String[] toReversePrimitiveArray(@NonNull LinkedHashSet emojiSet) {
- String[] emojis = new String[emojiSet.size()];
- int i = emojiSet.size() - 1;
- for (String emoji : emojiSet) {
- emojis[i--] = emoji;
- }
- return emojis;
+ // ..which we then save to shared prefs.
+ String recentlyUsedAsJsonString = JsonUtil.toJson(recentlyUsed);
+ boolean writeSuccess = prefs.edit().putString(RECENT_EMOJIS_KEY, recentlyUsedAsJsonString).commit();
+ if (!writeSuccess) { Log.w(TAG, "Failed to update recently used emojis in shared prefs."); }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java
deleted file mode 100644
index 3c3a4fa3eb..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java
+++ /dev/null
@@ -1,107 +0,0 @@
-package org.thoughtcrime.securesms.components.emoji.parsing;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.AssetManager;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.AsyncTask;
-import androidx.annotation.NonNull;
-import org.session.libsignal.utilities.Log;
-
-import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
-import org.thoughtcrime.securesms.util.Stopwatch;
-
-import org.session.libsession.utilities.ListenableFutureTask;
-import org.session.libsession.utilities.Util;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.SoftReference;
-import java.util.concurrent.Callable;
-
-public class EmojiPageBitmap {
-
- private static final String TAG = EmojiPageBitmap.class.getSimpleName();
-
- private final Context context;
- private final EmojiPageModel model;
- private final float decodeScale;
-
- private SoftReference bitmapReference;
- private ListenableFutureTask task;
-
- public EmojiPageBitmap(@NonNull Context context, @NonNull EmojiPageModel model, float decodeScale) {
- this.context = context.getApplicationContext();
- this.model = model;
- this.decodeScale = decodeScale;
- }
-
- @SuppressLint("StaticFieldLeak")
- public ListenableFutureTask get() {
- Util.assertMainThread();
-
- if (bitmapReference != null && bitmapReference.get() != null) {
- return new ListenableFutureTask<>(bitmapReference.get());
- } else if (task != null) {
- return task;
- } else {
- Callable callable = () -> {
- try {
- Log.i(TAG, "loading page " + model.getSpriteUri().toString());
- return loadPage();
- } catch (IOException ioe) {
- Log.w(TAG, ioe);
- }
- return null;
- };
- task = new ListenableFutureTask<>(callable);
- new AsyncTask() {
- @Override protected Void doInBackground(Void... params) {
- task.run();
- return null;
- }
-
- @Override protected void onPostExecute(Void aVoid) {
- task = null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- return task;
- }
-
- private Bitmap loadPage() throws IOException {
- if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get();
-
-
- float scale = decodeScale;
- AssetManager assetManager = context.getAssets();
- InputStream assetStream = assetManager.open(model.getSpriteUri().toString());
- BitmapFactory.Options options = new BitmapFactory.Options();
-
- if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) {
- Log.i(TAG, "Low memory detected. Changing sample size.");
- options.inSampleSize = 2;
- scale = decodeScale * 2;
- }
-
- Stopwatch stopwatch = new Stopwatch(model.getSpriteUri().toString());
- Bitmap bitmap = BitmapFactory.decodeStream(assetStream, null, options);
- stopwatch.split("decode");
-
- Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, (int)(bitmap.getWidth() * scale), (int)(bitmap.getHeight() * scale), true);
- stopwatch.split("scale");
- stopwatch.stop(TAG);
-
- bitmapReference = new SoftReference<>(scaledBitmap);
- Log.i(TAG, "onPageLoaded(" + model.getSpriteUri().toString() + ") originalByteCount: " + bitmap.getByteCount()
- + " scaledByteCount: " + scaledBitmap.getByteCount()
- + " scaledSize: " + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight());
- return scaledBitmap;
- }
-
- @Override
- public @NonNull String toString() {
- return model.getSpriteUri().toString();
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt
index 358a9d326b..700534fad1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt
@@ -1,12 +1,17 @@
package org.thoughtcrime.securesms.components.menu
+import android.content.Context
import androidx.annotation.AttrRes
+import androidx.annotation.ColorRes
/**
* Represents an action to be rendered
*/
data class ActionItem(
@AttrRes val iconRes: Int,
- val title: CharSequence,
- val action: Runnable
+ val title: Int,
+ val action: Runnable,
+ val contentDescription: Int? = null,
+ val subtitle: ((Context) -> CharSequence?)? = null,
+ @ColorRes val color: Int? = null,
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt
index 65fb1ddbbc..69dec0cdd6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt
@@ -1,13 +1,23 @@
package org.thoughtcrime.securesms.components.menu
+import android.content.Context
+import android.content.res.ColorStateList
import android.util.TypedValue
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
+import androidx.core.view.isGone
+import androidx.core.widget.ImageViewCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import network.loki.messenger.R
+import org.session.libsession.utilities.getColorFromAttr
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@@ -34,30 +44,23 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
mappingAdapter.submitList(items.toAdapterItems())
}
- private fun List.toAdapterItems(): List {
- return this.mapIndexed { index, item ->
- val displayType: DisplayType = when {
- this.size == 1 -> DisplayType.ONLY
+ private fun List.toAdapterItems(): List =
+ mapIndexed { index, item ->
+ when {
+ size == 1 -> DisplayType.ONLY
index == 0 -> DisplayType.TOP
- index == this.size - 1 -> DisplayType.BOTTOM
+ index == size - 1 -> DisplayType.BOTTOM
else -> DisplayType.MIDDLE
- }
-
- DisplayItem(item, displayType)
+ }.let { DisplayItem(item, it) }
}
- }
private data class DisplayItem(
val item: ActionItem,
val displayType: DisplayType
) : MappingModel {
- override fun areItemsTheSame(newItem: DisplayItem): Boolean {
- return this == newItem
- }
+ override fun areItemsTheSame(newItem: DisplayItem): Boolean = this == newItem
- override fun areContentsTheSame(newItem: DisplayItem): Boolean {
- return this == newItem
- }
+ override fun areContentsTheSame(newItem: DisplayItem): Boolean = this == newItem
}
private enum class DisplayType {
@@ -68,27 +71,61 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
itemView: View,
private val onItemClick: () -> Unit,
) : MappingViewHolder(itemView) {
+ private var subtitleJob: Job? = null
val icon: ImageView = itemView.findViewById(R.id.context_menu_item_icon)
val title: TextView = itemView.findViewById(R.id.context_menu_item_title)
+ val subtitle: TextView = itemView.findViewById(R.id.context_menu_item_subtitle)
override fun bind(model: DisplayItem) {
- if (model.item.iconRes > 0) {
+ val item = model.item
+ val color = item.color?.let { ContextCompat.getColor(context, it) }
+
+ if (item.iconRes > 0) {
val typedValue = TypedValue()
- context.theme.resolveAttribute(model.item.iconRes, typedValue, true)
+ context.theme.resolveAttribute(item.iconRes, typedValue, true)
icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId))
+
+ icon.imageTintList = ColorStateList.valueOf(color ?: context.getColorFromAttr(android.R.attr.textColor))
}
- title.text = model.item.title
+ item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it }
+ title.setText(item.title)
+ color?.let(title::setTextColor)
+ color?.let(subtitle::setTextColor)
+ subtitle.isGone = true
+ item.subtitle?.let { startSubtitleJob(subtitle, it) }
itemView.setOnClickListener {
- model.item.action.run()
+ item.action.run()
onItemClick()
}
when (model.displayType) {
- DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_top)
- DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_bottom)
- DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_middle)
- DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_only)
+ DisplayType.TOP -> R.drawable.context_menu_item_background_top
+ DisplayType.BOTTOM -> R.drawable.context_menu_item_background_bottom
+ DisplayType.MIDDLE -> R.drawable.context_menu_item_background_middle
+ DisplayType.ONLY -> R.drawable.context_menu_item_background_only
+ }.let(itemView::setBackgroundResource)
+ }
+
+ private fun startSubtitleJob(textView: TextView, getSubtitle: (Context) -> CharSequence?) {
+ fun updateText() = getSubtitle(context).let {
+ textView.isGone = it == null
+ textView.text = it
}
+ updateText()
+
+ subtitleJob?.cancel()
+ subtitleJob = CoroutineScope(Dispatchers.Main).launch {
+ while (true) {
+ updateText()
+ delay(200)
+ }
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ // naive job cancellation, will break if many items are added to context menu.
+ subtitleJob?.cancel()
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/SmoothScrollingLinearLayoutManager.java b/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/SmoothScrollingLinearLayoutManager.java
deleted file mode 100644
index a1b45ac2ae..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/SmoothScrollingLinearLayoutManager.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.thoughtcrime.securesms.components.recyclerview;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.LinearSmoothScroller;
-import android.util.DisplayMetrics;
-
-public class SmoothScrollingLinearLayoutManager extends LinearLayoutManager {
-
- public SmoothScrollingLinearLayoutManager(Context context, boolean reverseLayout) {
- super(context, LinearLayoutManager.VERTICAL, reverseLayout);
- }
-
- public void smoothScrollToPosition(@NonNull Context context, int position, float millisecondsPerInch) {
- final LinearSmoothScroller scroller = new LinearSmoothScroller(context) {
- @Override
- protected int getVerticalSnapPreference() {
- return LinearSmoothScroller.SNAP_TO_END;
- }
-
- @Override
- protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
- return millisecondsPerInch / displayMetrics.densityDpi;
- }
- };
-
- scroller.setTargetPosition(position);
- startSmoothScroll(scroller);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt
index 24637c4341..0b0ddf4b3d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt
@@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import network.loki.messenger.databinding.ContactSelectionListFragmentBinding
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
@@ -58,7 +57,6 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks> {
@@ -106,7 +95,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks): List {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
- it.address.isOpenGroup
+ it.address.isCommunity
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java
similarity index 89%
rename from app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java
rename to app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java
index 4a1059ffd9..5284fb0015 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms.contactshare;
+package org.thoughtcrime.securesms.contacts;
import android.content.Context;
import androidx.annotation.NonNull;
@@ -24,7 +24,7 @@ public final class ContactUtil {
return SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message));
}
- public static @NonNull String getDisplayName(@Nullable Contact contact) {
+ private static @NonNull String getDisplayName(@Nullable Contact contact) {
if (contact == null) {
return "";
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt
index 8f2675159b..538cd35077 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt
@@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.contacts
import android.app.Activity
import android.content.Intent
import android.os.Bundle
-import androidx.loader.app.LoaderManager
-import androidx.loader.content.Loader
-import androidx.recyclerview.widget.LinearLayoutManager
import android.view.Menu
import android.view.MenuItem
import android.view.View
+import androidx.loader.app.LoaderManager
+import androidx.loader.content.Loader
+import androidx.recyclerview.widget.LinearLayoutManager
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySelectContactsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
@@ -49,7 +49,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
LoaderManager.getInstance(this).initLoader(0, null, this)
}
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_done, menu)
return members.isNotEmpty()
}
@@ -70,7 +70,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
private fun update(members: List) {
this.members = members
- binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
+ binding.recyclerView.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
index e88cf1d08b..36a8c1adf5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
@@ -7,6 +7,7 @@ import android.view.View
import android.widget.LinearLayout
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewUserBinding
+import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
@@ -47,15 +48,14 @@ class UserView : LinearLayout {
// region Updating
fun bind(user: Recipient, glide: GlideRequests, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
+ val isLocalUser = user.isLocalNumber
fun getUserDisplayName(publicKey: String): String {
+ if (isLocalUser) return context.getString(R.string.MessageRecord_you)
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
- val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
- MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
val address = user.address.serialize()
- binding.profilePictureView.root.glide = glide
- binding.profilePictureView.root.update(user)
+ binding.profilePictureView.update(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
when (actionIndicator) {
@@ -87,7 +87,7 @@ class UserView : LinearLayout {
}
fun unbind() {
- binding.profilePictureView.root.recycle()
+ binding.profilePictureView.recycle()
}
// endregion
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactModelMapper.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactModelMapper.java
deleted file mode 100644
index ef783da791..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactModelMapper.java
+++ /dev/null
@@ -1,169 +0,0 @@
-package org.thoughtcrime.securesms.contactshare;
-
-import androidx.annotation.NonNull;
-
-import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
-import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
-import org.session.libsignal.utilities.guava.Optional;
-import org.session.libsignal.messages.SharedContact;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-
-import org.session.libsession.utilities.Contact;
-import static org.session.libsession.utilities.Contact.*;
-
-public class ContactModelMapper {
-
- public static SharedContact.Builder localToRemoteBuilder(@NonNull Contact contact) {
- List phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size());
- List emails = new ArrayList<>(contact.getEmails().size());
- List postalAddresses = new ArrayList<>(contact.getPostalAddresses().size());
-
- for (Phone phone : contact.getPhoneNumbers()) {
- phoneNumbers.add(new SharedContact.Phone.Builder().setValue(phone.getNumber())
- .setType(localToRemoteType(phone.getType()))
- .setLabel(phone.getLabel())
- .build());
- }
-
- for (Email email : contact.getEmails()) {
- emails.add(new SharedContact.Email.Builder().setValue(email.getEmail())
- .setType(localToRemoteType(email.getType()))
- .setLabel(email.getLabel())
- .build());
- }
-
- for (PostalAddress postalAddress : contact.getPostalAddresses()) {
- postalAddresses.add(new SharedContact.PostalAddress.Builder().setType(localToRemoteType(postalAddress.getType()))
- .setLabel(postalAddress.getLabel())
- .setStreet(postalAddress.getStreet())
- .setPobox(postalAddress.getPoBox())
- .setNeighborhood(postalAddress.getNeighborhood())
- .setCity(postalAddress.getCity())
- .setRegion(postalAddress.getRegion())
- .setPostcode(postalAddress.getPostalCode())
- .setCountry(postalAddress.getCountry())
- .build());
- }
-
- SharedContact.Name name = new SharedContact.Name.Builder().setDisplay(contact.getName().getDisplayName())
- .setGiven(contact.getName().getGivenName())
- .setFamily(contact.getName().getFamilyName())
- .setPrefix(contact.getName().getPrefix())
- .setSuffix(contact.getName().getSuffix())
- .setMiddle(contact.getName().getMiddleName())
- .build();
-
- return new SharedContact.Builder().setName(name)
- .withOrganization(contact.getOrganization())
- .withPhones(phoneNumbers)
- .withEmails(emails)
- .withAddresses(postalAddresses);
- }
-
- public static Contact remoteToLocal(@NonNull SharedContact sharedContact) {
- Name name = new Name(sharedContact.getName().getDisplay().orNull(),
- sharedContact.getName().getGiven().orNull(),
- sharedContact.getName().getFamily().orNull(),
- sharedContact.getName().getPrefix().orNull(),
- sharedContact.getName().getSuffix().orNull(),
- sharedContact.getName().getMiddle().orNull());
-
- List phoneNumbers = new LinkedList<>();
- if (sharedContact.getPhone().isPresent()) {
- for (SharedContact.Phone phone : sharedContact.getPhone().get()) {
- phoneNumbers.add(new Phone(phone.getValue(),
- remoteToLocalType(phone.getType()),
- phone.getLabel().orNull()));
- }
- }
-
- List emails = new LinkedList<>();
- if (sharedContact.getEmail().isPresent()) {
- for (SharedContact.Email email : sharedContact.getEmail().get()) {
- emails.add(new Email(email.getValue(),
- remoteToLocalType(email.getType()),
- email.getLabel().orNull()));
- }
- }
-
- List postalAddresses = new LinkedList<>();
- if (sharedContact.getAddress().isPresent()) {
- for (SharedContact.PostalAddress postalAddress : sharedContact.getAddress().get()) {
- postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()),
- postalAddress.getLabel().orNull(),
- postalAddress.getStreet().orNull(),
- postalAddress.getPobox().orNull(),
- postalAddress.getNeighborhood().orNull(),
- postalAddress.getCity().orNull(),
- postalAddress.getRegion().orNull(),
- postalAddress.getPostcode().orNull(),
- postalAddress.getCountry().orNull()));
- }
- }
-
- Avatar avatar = null;
- if (sharedContact.getAvatar().isPresent()) {
- Attachment attachment = PointerAttachment.forPointer(Optional.of(sharedContact.getAvatar().get().getAttachment().asPointer())).get();
- boolean isProfile = sharedContact.getAvatar().get().isProfile();
-
- avatar = new Avatar(null, attachment, isProfile);
- }
-
- return new Contact(name, sharedContact.getOrganization().orNull(), phoneNumbers, emails, postalAddresses, avatar);
- }
-
- private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) {
- switch (type) {
- case HOME: return Phone.Type.HOME;
- case MOBILE: return Phone.Type.MOBILE;
- case WORK: return Phone.Type.WORK;
- default: return Phone.Type.CUSTOM;
- }
- }
-
- private static Email.Type remoteToLocalType(SharedContact.Email.Type type) {
- switch (type) {
- case HOME: return Email.Type.HOME;
- case MOBILE: return Email.Type.MOBILE;
- case WORK: return Email.Type.WORK;
- default: return Email.Type.CUSTOM;
- }
- }
-
- private static PostalAddress.Type remoteToLocalType(SharedContact.PostalAddress.Type type) {
- switch (type) {
- case HOME: return PostalAddress.Type.HOME;
- case WORK: return PostalAddress.Type.WORK;
- default: return PostalAddress.Type.CUSTOM;
- }
- }
-
- private static SharedContact.Phone.Type localToRemoteType(Phone.Type type) {
- switch (type) {
- case HOME: return SharedContact.Phone.Type.HOME;
- case MOBILE: return SharedContact.Phone.Type.MOBILE;
- case WORK: return SharedContact.Phone.Type.WORK;
- default: return SharedContact.Phone.Type.CUSTOM;
- }
- }
-
- private static SharedContact.Email.Type localToRemoteType(Email.Type type) {
- switch (type) {
- case HOME: return SharedContact.Email.Type.HOME;
- case MOBILE: return SharedContact.Email.Type.MOBILE;
- case WORK: return SharedContact.Email.Type.WORK;
- default: return SharedContact.Email.Type.CUSTOM;
- }
- }
-
- private static SharedContact.PostalAddress.Type localToRemoteType(PostalAddress.Type type) {
- switch (type) {
- case HOME: return SharedContact.PostalAddress.Type.HOME;
- case WORK: return SharedContact.PostalAddress.Type.WORK;
- default: return SharedContact.PostalAddress.Type.CUSTOM;
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt
new file mode 100644
index 0000000000..184869b9ad
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt
@@ -0,0 +1,184 @@
+package org.thoughtcrime.securesms.conversation
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
+import com.google.android.material.tabs.TabLayoutMediator
+import dagger.hilt.android.AndroidEntryPoint
+import network.loki.messenger.R
+import network.loki.messenger.databinding.ViewConversationActionBarBinding
+import network.loki.messenger.databinding.ViewConversationSettingBinding
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.session.libsession.messaging.messages.ExpirationConfiguration
+import org.session.libsession.messaging.open_groups.OpenGroup
+import org.session.libsession.utilities.ExpirationUtil
+import org.session.libsession.utilities.modifyLayoutParams
+import org.session.libsession.utilities.recipients.Recipient
+import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
+import org.thoughtcrime.securesms.database.GroupDatabase
+import org.thoughtcrime.securesms.database.LokiAPIDatabase
+import org.thoughtcrime.securesms.util.DateUtils
+import java.util.Locale
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ConversationActionBarView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+ private val binding = ViewConversationActionBarBinding.inflate(LayoutInflater.from(context), this, true)
+
+ @Inject lateinit var lokiApiDb: LokiAPIDatabase
+ @Inject lateinit var groupDb: GroupDatabase
+
+ var delegate: ConversationActionBarDelegate? = null
+
+ private val settingsAdapter = ConversationSettingsAdapter { setting ->
+ if (setting.settingType == ConversationSettingType.EXPIRATION) {
+ delegate?.onDisappearingMessagesClicked()
+ }
+ }
+
+ init {
+ var previousState: Int
+ var currentState = 0
+ binding.settingsPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
+ override fun onPageScrollStateChanged(state: Int) {
+ val currentPage: Int = binding.settingsPager.currentItem
+ val lastPage = maxOf( (binding.settingsPager.adapter?.itemCount ?: 0) - 1, 0)
+ if (currentPage == lastPage || currentPage == 0) {
+ previousState = currentState
+ currentState = state
+ if (previousState == 1 && currentState == 0) {
+ binding.settingsPager.setCurrentItem(if (currentPage == 0) lastPage else 0, true)
+ }
+ }
+ }
+ })
+ binding.settingsPager.adapter = settingsAdapter
+ TabLayoutMediator(binding.settingsTabLayout, binding.settingsPager) { _, _ -> }.attach()
+ }
+
+ fun bind(
+ delegate: ConversationActionBarDelegate,
+ threadId: Long,
+ recipient: Recipient,
+ config: ExpirationConfiguration? = null,
+ openGroup: OpenGroup? = null
+ ) {
+ this.delegate = delegate
+ binding.profilePictureView.layoutParams = resources.getDimensionPixelSize(
+ if (recipient.isClosedGroupRecipient) R.dimen.medium_profile_picture_size else R.dimen.small_profile_picture_size
+ ).let { LayoutParams(it, it) }
+ MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadId, context)
+ update(recipient, openGroup, config)
+ }
+
+ fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
+ binding.profilePictureView.update(recipient)
+ binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.note_to_self)
+ updateSubtitle(recipient, openGroup, config)
+
+ binding.conversationTitleContainer.modifyLayoutParams {
+ marginEnd = if (recipient.showCallMenu()) 0 else binding.profilePictureView.width
+ }
+ }
+
+ fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
+ val settings = mutableListOf()
+ if (config?.isEnabled == true) {
+ val prefix = when (config.expiryMode) {
+ is ExpiryMode.AfterRead -> R.string.expiration_type_disappear_after_read
+ else -> R.string.expiration_type_disappear_after_send
+ }.let(context::getString)
+ settings += ConversationSetting(
+ "$prefix - ${ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, config.expiryMode.expirySeconds)}",
+ ConversationSettingType.EXPIRATION,
+ R.drawable.ic_timer,
+ resources.getString(R.string.AccessibilityId_disappearing_messages_type_and_time)
+ )
+ }
+ if (recipient.isMuted) {
+ settings += ConversationSetting(
+ recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE }
+ ?.let { context.getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(it, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())) }
+ ?: context.getString(R.string.ConversationActivity_muted_forever),
+ ConversationSettingType.NOTIFICATION,
+ R.drawable.ic_outline_notifications_off_24
+ )
+ }
+ if (recipient.isGroupRecipient) {
+ val title = if (recipient.isCommunityRecipient) {
+ val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
+ context.getString(R.string.ConversationActivity_active_member_count, userCount)
+ } else {
+ val userCount = groupDb.getGroupMemberAddresses(recipient.address.toGroupString(), true).size
+ context.getString(R.string.ConversationActivity_member_count, userCount)
+ }
+ settings += ConversationSetting(title, ConversationSettingType.MEMBER_COUNT)
+ }
+ settingsAdapter.submitList(settings)
+ binding.settingsTabLayout.isVisible = settings.size > 1
+ }
+
+ class ConversationSettingsAdapter(
+ private val settingsListener: (ConversationSetting) -> Unit
+ ) : ListAdapter(SettingsDiffer()) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
+ val layoutInflater = LayoutInflater.from(parent.context)
+ return SettingViewHolder(ViewConversationSettingBinding.inflate(layoutInflater, parent, false))
+ }
+
+ override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
+ holder.bind(getItem(position), itemCount) {
+ settingsListener.invoke(it)
+ }
+ }
+
+ class SettingViewHolder(
+ private val binding: ViewConversationSettingBinding
+ ): RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(setting: ConversationSetting, itemCount: Int, listener: (ConversationSetting) -> Unit) {
+ binding.root.setOnClickListener { listener.invoke(setting) }
+ binding.root.contentDescription = setting.contentDescription
+ binding.iconImageView.setImageResource(setting.iconResId)
+ binding.iconImageView.isVisible = setting.iconResId > 0
+ binding.titleView.text = setting.title
+ binding.leftArrowImageView.isVisible = itemCount > 1
+ binding.rightArrowImageView.isVisible = itemCount > 1
+ }
+ }
+
+ class SettingsDiffer: DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ConversationSetting, newItem: ConversationSetting): Boolean = oldItem.settingType === newItem.settingType
+ override fun areContentsTheSame(oldItem: ConversationSetting, newItem: ConversationSetting): Boolean = oldItem == newItem
+ }
+ }
+}
+
+fun interface ConversationActionBarDelegate {
+ fun onDisappearingMessagesClicked()
+}
+
+data class ConversationSetting(
+ val title: String,
+ val settingType: ConversationSettingType,
+ val iconResId: Int = 0,
+ val contentDescription: String = ""
+)
+
+enum class ConversationSettingType {
+ EXPIRATION,
+ MEMBER_COUNT,
+ NOTIFICATION
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt
new file mode 100644
index 0000000000..d336c967ce
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt
@@ -0,0 +1,72 @@
+package org.thoughtcrime.securesms.conversation.disappearingmessages
+
+import android.content.Context
+import dagger.hilt.android.qualifiers.ApplicationContext
+import network.loki.messenger.R
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.messaging.messages.ExpirationConfiguration
+import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
+import org.session.libsession.messaging.sending_receiving.MessageSender
+import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.ExpirationUtil
+import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.getExpirationTypeDisplayValue
+import org.thoughtcrime.securesms.database.ThreadDatabase
+import org.thoughtcrime.securesms.database.model.MessageRecord
+import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+
+class DisappearingMessages @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val textSecurePreferences: TextSecurePreferences,
+ private val messageExpirationManager: MessageExpirationManagerProtocol,
+) {
+ fun set(threadId: Long, address: Address, mode: ExpiryMode, isGroup: Boolean) {
+ val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
+ MessagingModuleConfiguration.shared.storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs))
+
+ val message = ExpirationTimerUpdate(isGroup = isGroup).apply {
+ expiryMode = mode
+ sender = textSecurePreferences.getLocalNumber()
+ isSenderSelf = true
+ recipient = address.serialize()
+ sentTimestamp = expiryChangeTimestampMs
+ }
+
+ messageExpirationManager.insertExpirationTimerMessage(message)
+ MessageSender.send(message, address)
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ }
+
+ fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog {
+ title(R.string.dialog_disappearing_messages_follow_setting_title)
+ text(if (message.expiresIn == 0L) {
+ context.getString(R.string.dialog_disappearing_messages_follow_setting_off_body)
+ } else {
+ context.getString(
+ R.string.dialog_disappearing_messages_follow_setting_on_body,
+ ExpirationUtil.getExpirationDisplayValue(
+ context,
+ message.expiresIn.milliseconds
+ ),
+ context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead)
+ )
+ })
+ destructiveButton(
+ text = if (message.expiresIn == 0L) R.string.dialog_disappearing_messages_follow_setting_confirm else R.string.dialog_disappearing_messages_follow_setting_set,
+ contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_set_button
+ ) {
+ set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient)
+ }
+ cancelButton()
+ }
+}
+
+val MessageRecord.expiryMode get() = if (expiresIn <= 0) ExpiryMode.NONE
+ else if (expireStarted == timestamp) ExpiryMode.AfterSend(expiresIn / 1000)
+ else ExpiryMode.AfterRead(expiresIn / 1000)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt
new file mode 100644
index 0000000000..16e74cdde9
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt
@@ -0,0 +1,94 @@
+package org.thoughtcrime.securesms.conversation.disappearingmessages
+
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+import network.loki.messenger.R
+import network.loki.messenger.databinding.ActivityDisappearingMessagesBinding
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
+import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.DisappearingMessages
+import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.UiState
+import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.database.ThreadDatabase
+import org.thoughtcrime.securesms.ui.AppTheme
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
+
+ private lateinit var binding : ActivityDisappearingMessagesBinding
+
+ @Inject lateinit var recipientDb: RecipientDatabase
+ @Inject lateinit var threadDb: ThreadDatabase
+ @Inject lateinit var viewModelFactory: DisappearingMessagesViewModel.AssistedFactory
+
+ private val threadId: Long by lazy {
+ intent.getLongExtra(THREAD_ID, -1)
+ }
+
+ private val viewModel: DisappearingMessagesViewModel by viewModels {
+ viewModelFactory.create(threadId)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
+ super.onCreate(savedInstanceState, ready)
+ binding = ActivityDisappearingMessagesBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setUpToolbar()
+
+ binding.container.setContent { DisappearingMessagesScreen() }
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.event.collect {
+ when (it) {
+ Event.SUCCESS -> finish()
+ Event.FAIL -> showToast(getString(R.string.DisappearingMessagesActivity_settings_not_updated))
+ }
+ }
+ }
+ }
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.state.collect {
+ supportActionBar?.subtitle = it.subtitle(this@DisappearingMessagesActivity)
+ }
+ }
+ }
+ }
+
+ private fun showToast(message: String) {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+ }
+
+ private fun setUpToolbar() {
+ setSupportActionBar(binding.toolbar)
+ supportActionBar?.apply {
+ title = getString(R.string.activity_disappearing_messages_title)
+ setDisplayHomeAsUpEnabled(true)
+ setHomeButtonEnabled(true)
+ }
+ }
+
+ companion object {
+ const val THREAD_ID = "thread_id"
+ }
+
+ @Composable
+ fun DisappearingMessagesScreen() {
+ val uiState by viewModel.uiState.collectAsState(UiState())
+ AppTheme {
+ DisappearingMessages(uiState, callbacks = viewModel)
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt
new file mode 100644
index 0000000000..32e20b73d9
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt
@@ -0,0 +1,129 @@
+package org.thoughtcrime.securesms.conversation.disappearingmessages
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import network.loki.messenger.BuildConfig
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.session.libsession.messaging.messages.ExpirationConfiguration
+import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
+import org.session.libsession.utilities.TextSecurePreferences
+import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.ExpiryCallbacks
+import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.UiState
+import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.toUiState
+import org.thoughtcrime.securesms.database.GroupDatabase
+import org.thoughtcrime.securesms.database.Storage
+import org.thoughtcrime.securesms.database.ThreadDatabase
+
+class DisappearingMessagesViewModel(
+ private val threadId: Long,
+ private val application: Application,
+ private val textSecurePreferences: TextSecurePreferences,
+ private val messageExpirationManager: MessageExpirationManagerProtocol,
+ private val disappearingMessages: DisappearingMessages,
+ private val threadDb: ThreadDatabase,
+ private val groupDb: GroupDatabase,
+ private val storage: Storage,
+ isNewConfigEnabled: Boolean,
+ showDebugOptions: Boolean
+) : AndroidViewModel(application), ExpiryCallbacks {
+
+ private val _event = Channel()
+ val event = _event.receiveAsFlow()
+
+ private val _state = MutableStateFlow(
+ State(
+ isNewConfigEnabled = isNewConfigEnabled,
+ showDebugOptions = showDebugOptions
+ )
+ )
+ val state = _state.asStateFlow()
+
+ val uiState = _state
+ .map(State::toUiState)
+ .stateIn(viewModelScope, SharingStarted.Eagerly, UiState())
+
+ init {
+ viewModelScope.launch {
+ val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode?.maybeConvertToLegacy(isNewConfigEnabled) ?: ExpiryMode.NONE
+ val recipient = threadDb.getRecipientForThreadId(threadId)
+ val groupRecord = recipient?.takeIf { it.isClosedGroupRecipient }
+ ?.run { groupDb.getGroup(address.toGroupString()).orNull() }
+
+ _state.update {
+ it.copy(
+ address = recipient?.address,
+ isGroup = groupRecord != null,
+ isNoteToSelf = recipient?.address?.serialize() == textSecurePreferences.getLocalNumber(),
+ isSelfAdmin = groupRecord == null || groupRecord.admins.any{ it.serialize() == textSecurePreferences.getLocalNumber() },
+ expiryMode = expiryMode,
+ persistedMode = expiryMode
+ )
+ }
+ }
+ }
+
+ override fun setValue(value: ExpiryMode) = _state.update { it.copy(expiryMode = value) }
+
+ override fun onSetClick() = viewModelScope.launch {
+ val state = _state.value
+ val mode = state.expiryMode?.coerceLegacyToAfterSend()
+ val address = state.address
+ if (address == null || mode == null) {
+ _event.send(Event.FAIL)
+ return@launch
+ }
+
+ disappearingMessages.set(threadId, address, mode, state.isGroup)
+
+ _event.send(Event.SUCCESS)
+ }
+
+ private fun ExpiryMode.coerceLegacyToAfterSend() = takeUnless { it is ExpiryMode.Legacy } ?: ExpiryMode.AfterSend(expirySeconds)
+
+ @dagger.assisted.AssistedFactory
+ interface AssistedFactory {
+ fun create(threadId: Long): Factory
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ class Factory @AssistedInject constructor(
+ @Assisted private val threadId: Long,
+ private val application: Application,
+ private val textSecurePreferences: TextSecurePreferences,
+ private val messageExpirationManager: MessageExpirationManagerProtocol,
+ private val disappearingMessages: DisappearingMessages,
+ private val threadDb: ThreadDatabase,
+ private val groupDb: GroupDatabase,
+ private val storage: Storage
+ ) : ViewModelProvider.Factory {
+
+ override fun create(modelClass: Class): T = DisappearingMessagesViewModel(
+ threadId,
+ application,
+ textSecurePreferences,
+ messageExpirationManager,
+ disappearingMessages,
+ threadDb,
+ groupDb,
+ storage,
+ ExpirationConfiguration.isNewConfigEnabled,
+ BuildConfig.DEBUG
+ ) as T
+ }
+}
+
+private fun ExpiryMode.maybeConvertToLegacy(isNewConfigEnabled: Boolean): ExpiryMode = takeIf { isNewConfigEnabled } ?: ExpiryMode.Legacy(expirySeconds)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt
new file mode 100644
index 0000000000..ced4cc0035
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt
@@ -0,0 +1,90 @@
+package org.thoughtcrime.securesms.conversation.disappearingmessages
+
+import androidx.annotation.StringRes
+import network.loki.messenger.R
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.session.libsession.utilities.Address
+import org.thoughtcrime.securesms.ui.GetString
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.hours
+
+enum class Event {
+ SUCCESS, FAIL
+}
+
+data class State(
+ val isGroup: Boolean = false,
+ val isSelfAdmin: Boolean = true,
+ val address: Address? = null,
+ val isNoteToSelf: Boolean = false,
+ val expiryMode: ExpiryMode? = null,
+ val isNewConfigEnabled: Boolean = true,
+ val persistedMode: ExpiryMode? = null,
+ val showDebugOptions: Boolean = false
+) {
+ val subtitle get() = when {
+ isGroup || isNoteToSelf -> GetString(R.string.activity_disappearing_messages_subtitle_sent)
+ else -> GetString(R.string.activity_disappearing_messages_subtitle)
+ }
+
+ val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
+
+ val nextType get() = when {
+ expiryType == ExpiryType.AFTER_READ -> ExpiryType.AFTER_READ
+ isNewConfigEnabled -> ExpiryType.AFTER_SEND
+ else -> ExpiryType.LEGACY
+ }
+
+ val duration get() = expiryMode?.duration
+ val expiryType get() = expiryMode?.type
+
+ val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
+}
+
+
+enum class ExpiryType(
+ private val createMode: (Long) -> ExpiryMode,
+ @StringRes val title: Int,
+ @StringRes val subtitle: Int? = null,
+ @StringRes val contentDescription: Int = title,
+) {
+ NONE(
+ { ExpiryMode.NONE },
+ R.string.expiration_off,
+ contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
+ ),
+ LEGACY(
+ ExpiryMode::Legacy,
+ R.string.expiration_type_disappear_legacy,
+ contentDescription = R.string.expiration_type_disappear_legacy_description
+ ),
+ AFTER_READ(
+ ExpiryMode::AfterRead,
+ R.string.expiration_type_disappear_after_read,
+ R.string.expiration_type_disappear_after_read_description,
+ R.string.AccessibilityId_disappear_after_read_option
+ ),
+ AFTER_SEND(
+ ExpiryMode::AfterSend,
+ R.string.expiration_type_disappear_after_send,
+ R.string.expiration_type_disappear_after_send_description,
+ R.string.AccessibilityId_disappear_after_send_option
+ );
+
+ fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE
+ fun mode(duration: Duration) = mode(duration.inWholeSeconds)
+
+ fun defaultMode(persistedMode: ExpiryMode?) = when(this) {
+ persistedMode?.type -> persistedMode
+ AFTER_READ -> mode(12.hours)
+ else -> mode(1.days)
+ }
+}
+
+val ExpiryMode.type: ExpiryType get() = when(this) {
+ is ExpiryMode.Legacy -> ExpiryType.LEGACY
+ is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND
+ is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ
+ else -> ExpiryType.NONE
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt
new file mode 100644
index 0000000000..6ddc28c688
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt
@@ -0,0 +1,98 @@
+package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
+
+import network.loki.messenger.R
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
+import org.thoughtcrime.securesms.conversation.disappearingmessages.State
+import org.thoughtcrime.securesms.ui.GetString
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+fun State.toUiState() = UiState(
+ cards = listOfNotNull(
+ typeOptions()?.let { ExpiryOptionsCard(GetString(R.string.activity_disappearing_messages_delete_type), it) },
+ timeOptions()?.let { ExpiryOptionsCard(GetString(R.string.activity_disappearing_messages_timer), it) }
+ ),
+ showGroupFooter = isGroup && isNewConfigEnabled,
+ showSetButton = isSelfAdmin
+)
+
+private fun State.typeOptions(): List? = if (typeOptionsHidden) null else {
+ buildList {
+ add(offTypeOption())
+ if (!isNewConfigEnabled) add(legacyTypeOption())
+ if (!isGroup) add(afterReadTypeOption())
+ add(afterSendTypeOption())
+ }
+}
+
+private fun State.timeOptions(): List? {
+ // Don't show times card if we have a types card, and type is off.
+ if (!typeOptionsHidden && expiryType == ExpiryType.NONE) return null
+
+ return nextType.let { type ->
+ when (type) {
+ ExpiryType.AFTER_READ -> afterReadTimes
+ else -> afterSendTimes
+ }.map { timeOption(type, it) }
+ }.let {
+ buildList {
+ if (typeOptionsHidden) add(offTypeOption())
+ addAll(debugOptions())
+ addAll(it)
+ }
+ }
+}
+
+private fun State.offTypeOption() = typeOption(ExpiryType.NONE)
+private fun State.legacyTypeOption() = typeOption(ExpiryType.LEGACY)
+private fun State.afterReadTypeOption() = newTypeOption(ExpiryType.AFTER_READ)
+private fun State.afterSendTypeOption() = newTypeOption(ExpiryType.AFTER_SEND)
+private fun State.newTypeOption(type: ExpiryType) = typeOption(type, isNewConfigEnabled && isSelfAdmin)
+
+private fun State.typeOption(
+ type: ExpiryType,
+ enabled: Boolean = isSelfAdmin,
+) = ExpiryRadioOption(
+ value = type.defaultMode(persistedMode),
+ title = GetString(type.title),
+ subtitle = type.subtitle?.let(::GetString),
+ contentDescription = GetString(type.contentDescription),
+ selected = expiryType == type,
+ enabled = enabled
+)
+
+private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 30.seconds, 1.minutes) else emptyList()
+private fun debugModes(isDebug: Boolean, type: ExpiryType) =
+ debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
+private fun State.debugOptions(): List =
+ debugModes(showDebugOptions, nextType).map { timeOption(it, subtitle = GetString("for testing purposes")) }
+
+private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
+
+private val afterReadTimes = buildList {
+ add(5.minutes)
+ add(1.hours)
+ addAll(afterSendTimes)
+}
+
+private fun State.timeOption(
+ type: ExpiryType,
+ time: Duration
+) = timeOption(type.mode(time))
+
+private fun State.timeOption(
+ mode: ExpiryMode,
+ title: GetString = GetString(mode.duration),
+ subtitle: GetString? = null,
+) = ExpiryRadioOption(
+ value = mode,
+ title = title,
+ subtitle = subtitle,
+ contentDescription = title,
+ selected = mode.duration == expiryMode?.duration,
+ enabled = isTimeOptionsEnabled
+)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt
new file mode 100644
index 0000000000..3fec60a0a3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt
@@ -0,0 +1,75 @@
+package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import network.loki.messenger.R
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.thoughtcrime.securesms.ui.Callbacks
+import org.thoughtcrime.securesms.ui.GetString
+import org.thoughtcrime.securesms.ui.NoOpCallbacks
+import org.thoughtcrime.securesms.ui.OptionsCard
+import org.thoughtcrime.securesms.ui.OutlineButton
+import org.thoughtcrime.securesms.ui.RadioOption
+import org.thoughtcrime.securesms.ui.contentDescription
+import org.thoughtcrime.securesms.ui.fadingEdges
+
+typealias ExpiryCallbacks = Callbacks
+typealias ExpiryRadioOption = RadioOption
+
+@Composable
+fun DisappearingMessages(
+ state: UiState,
+ modifier: Modifier = Modifier,
+ callbacks: ExpiryCallbacks = NoOpCallbacks
+) {
+ val scrollState = rememberScrollState()
+
+ Column(modifier = modifier.padding(horizontal = 32.dp)) {
+ Box(modifier = Modifier.weight(1f)) {
+ Column(
+ modifier = Modifier
+ .padding(bottom = 20.dp)
+ .verticalScroll(scrollState)
+ .fadingEdges(scrollState),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ state.cards.forEach {
+ OptionsCard(it, callbacks)
+ }
+
+ if (state.showGroupFooter) Text(text = stringResource(R.string.activity_disappearing_messages_group_footer),
+ style = TextStyle(
+ fontSize = 11.sp,
+ fontWeight = FontWeight(400),
+ color = Color(0xFFA1A2A1),
+ textAlign = TextAlign.Center),
+ modifier = Modifier.fillMaxWidth())
+ }
+ }
+
+ if (state.showSetButton) OutlineButton(
+ GetString(R.string.disappearing_messages_set_button_title),
+ modifier = Modifier
+ .contentDescription(GetString(R.string.AccessibilityId_set_button))
+ .align(Alignment.CenterHorizontally)
+ .padding(bottom = 20.dp),
+ onClick = callbacks::onSetClick
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt
new file mode 100644
index 0000000000..c2524bf261
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt
@@ -0,0 +1,62 @@
+package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import network.loki.messenger.R
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
+import org.thoughtcrime.securesms.conversation.disappearingmessages.State
+import org.thoughtcrime.securesms.ui.PreviewTheme
+import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
+
+@Preview(widthDp = 450, heightDp = 700)
+@Composable
+fun PreviewStates(
+ @PreviewParameter(StatePreviewParameterProvider::class) state: State
+) {
+ PreviewTheme(R.style.Classic_Dark) {
+ DisappearingMessages(
+ state.toUiState()
+ )
+ }
+}
+
+class StatePreviewParameterProvider : PreviewParameterProvider {
+ override val values = newConfigValues.filter { it.expiryType != ExpiryType.LEGACY } + newConfigValues.map { it.copy(isNewConfigEnabled = false) }
+
+ private val newConfigValues get() = sequenceOf(
+ // new 1-1
+ State(expiryMode = ExpiryMode.NONE),
+ State(expiryMode = ExpiryMode.Legacy(43200)),
+ State(expiryMode = ExpiryMode.AfterRead(300)),
+ State(expiryMode = ExpiryMode.AfterSend(43200)),
+ // new group non-admin
+ State(isGroup = true, isSelfAdmin = false),
+ State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.Legacy(43200)),
+ State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.AfterSend(43200)),
+ // new group admin
+ State(isGroup = true),
+ State(isGroup = true, expiryMode = ExpiryMode.Legacy(43200)),
+ State(isGroup = true, expiryMode = ExpiryMode.AfterSend(43200)),
+ // new note-to-self
+ State(isNoteToSelf = true),
+ )
+}
+
+@Preview
+@Composable
+fun PreviewThemes(
+ @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
+) {
+ PreviewTheme(themeResId) {
+ DisappearingMessages(
+ State(expiryMode = ExpiryMode.AfterSend(43200)).toUiState(),
+ modifier = Modifier.size(400.dp, 600.dp)
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/UiState.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/UiState.kt
new file mode 100644
index 0000000000..40f917427c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/UiState.kt
@@ -0,0 +1,32 @@
+package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
+
+import androidx.annotation.StringRes
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.thoughtcrime.securesms.ui.GetString
+import org.thoughtcrime.securesms.ui.RadioOption
+
+typealias ExpiryOptionsCard = OptionsCard
+
+data class UiState(
+ val cards: List = emptyList(),
+ val showGroupFooter: Boolean = false,
+ val showSetButton: Boolean = true
+) {
+ constructor(
+ vararg cards: ExpiryOptionsCard,
+ showGroupFooter: Boolean = false,
+ showSetButton: Boolean = true,
+ ): this(
+ cards.asList(),
+ showGroupFooter,
+ showSetButton
+ )
+}
+
+data class OptionsCard(
+ val title: GetString,
+ val options: List>
+) {
+ constructor(title: GetString, vararg options: RadioOption): this(title, options.asList())
+ constructor(@StringRes title: Int, vararg options: RadioOption): this(GetString(title), options.asList())
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/paging/ConversationPager.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/paging/ConversationPager.kt
new file mode 100644
index 0000000000..827c394546
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/paging/ConversationPager.kt
@@ -0,0 +1,129 @@
+package org.thoughtcrime.securesms.conversation.paging
+
+import androidx.annotation.WorkerThread
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import androidx.recyclerview.widget.DiffUtil
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.session.libsession.messaging.contacts.Contact
+import org.thoughtcrime.securesms.database.MmsSmsDatabase
+import org.thoughtcrime.securesms.database.SessionContactDatabase
+import org.thoughtcrime.securesms.database.model.MessageRecord
+
+private const val TIME_BUCKET = 600000L // bucket into 10 minute increments
+
+private fun config() = PagingConfig(
+ pageSize = 25,
+ maxSize = 100,
+ enablePlaceholders = false
+)
+
+fun Long.bucketed(): Long = (TIME_BUCKET - this % TIME_BUCKET) + this
+
+fun conversationPager(threadId: Long, initialKey: PageLoad? = null, db: MmsSmsDatabase, contactDb: SessionContactDatabase) = Pager(config(), initialKey = initialKey) {
+ ConversationPagingSource(threadId, db, contactDb)
+}
+
+class ConversationPagerDiffCallback: DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: MessageAndContact, newItem: MessageAndContact): Boolean =
+ oldItem.message.id == newItem.message.id && oldItem.message.isMms == newItem.message.isMms
+
+ override fun areContentsTheSame(oldItem: MessageAndContact, newItem: MessageAndContact): Boolean =
+ oldItem == newItem
+}
+
+data class MessageAndContact(val message: MessageRecord,
+ val contact: Contact?)
+
+data class PageLoad(val fromTime: Long, val toTime: Long? = null)
+
+class ConversationPagingSource(
+ private val threadId: Long,
+ private val messageDb: MmsSmsDatabase,
+ private val contactDb: SessionContactDatabase
+ ): PagingSource() {
+
+ override fun getRefreshKey(state: PagingState): PageLoad? {
+ val anchorPosition = state.anchorPosition ?: return null
+ val anchorPage = state.closestPageToPosition(anchorPosition) ?: return null
+ val next = anchorPage.nextKey?.fromTime
+ val previous = anchorPage.prevKey?.fromTime ?: anchorPage.data.firstOrNull()?.message?.dateSent ?: return null
+ return PageLoad(previous, next)
+ }
+
+ private val contactCache = mutableMapOf()
+
+ @WorkerThread
+ private fun getContact(sessionId: String): Contact? {
+ contactCache[sessionId]?.let { contact ->
+ return contact
+ } ?: run {
+ contactDb.getContactWithSessionID(sessionId)?.let { contact ->
+ contactCache[sessionId] = contact
+ return contact
+ }
+ }
+ return null
+ }
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ val pageLoad = params.key ?: withContext(Dispatchers.IO) {
+ messageDb.getConversationSnippet(threadId).use {
+ val reader = messageDb.readerFor(it)
+ var record: MessageRecord? = null
+ if (reader != null) {
+ record = reader.next
+ while (record != null && record.isDeleted) {
+ record = reader.next
+ }
+ }
+ record?.dateSent?.let { fromTime ->
+ PageLoad(fromTime)
+ }
+ }
+ } ?: return LoadResult.Page(emptyList(), null, null)
+
+ val result = withContext(Dispatchers.IO) {
+ val cursor = messageDb.getConversationPage(
+ threadId,
+ pageLoad.fromTime,
+ pageLoad.toTime ?: -1L,
+ params.loadSize
+ )
+ val processedList = mutableListOf()
+ val reader = messageDb.readerFor(cursor)
+ while (reader.next != null && !invalid) {
+ reader.current?.let { item ->
+ val contact = getContact(item.individualRecipient.address.serialize())
+ processedList += MessageAndContact(item, contact)
+ }
+ }
+ reader.close()
+ processedList.toMutableList()
+ }
+
+ val hasNext = withContext(Dispatchers.IO) {
+ if (result.isEmpty()) return@withContext false
+ val lastTime = result.last().message.dateSent
+ messageDb.hasNextPage(threadId, lastTime)
+ }
+
+ val nextCheckTime = if (hasNext) {
+ val lastSent = result.last().message.dateSent
+ if (lastSent == pageLoad.fromTime) null else lastSent
+ } else null
+
+ val hasPrevious = withContext(Dispatchers.IO) { messageDb.hasPreviousPage(threadId, pageLoad.fromTime) }
+ val nextKey = if (!hasNext) null else nextCheckTime
+ val prevKey = if (!hasPrevious) null else messageDb.getPreviousPage(threadId, pageLoad.fromTime, params.loadSize)
+
+ return LoadResult.Page(
+ data = result, // next check time is not null if drop is true
+ prevKey = prevKey?.let { PageLoad(it, pageLoad.fromTime) },
+ nextKey = nextKey?.let { PageLoad(it) }
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
index 99e7c90615..df2bc1c371 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
@@ -32,15 +32,31 @@ class ContactListAdapter(
class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) {
- binding.profilePictureView.root.glide = glide
- binding.profilePictureView.root.update(contact.recipient)
+ binding.profilePictureView.update(contact.recipient)
binding.nameTextView.text = contact.displayName
binding.root.setOnClickListener { listener(contact.recipient) }
+
+ // TODO: When we implement deleting contacts (hide might be safest for now) then probably set a long-click listener here w/ something like:
+ /*
+ binding.root.setOnLongClickListener {
+ Log.w("[ACL]", "Long clicked on contact ${contact.recipient.name}")
+ binding.contentView.context.showSessionDialog {
+ title("Delete Contact")
+ text("Are you sure you want to delete this contact?")
+ button(R.string.delete) {
+ val contacts = configFactory.contacts ?: return
+ contacts.upsertContact(contact.recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ endActionMode()
+ }
+ cancelButton(::endActionMode)
+ }
+ true
+ }
+ */
}
- fun unbind() {
- binding.profilePictureView.root.recycle()
- }
+ fun unbind() { binding.profilePictureView.recycle() }
}
class HeaderViewHolder(
@@ -53,15 +69,11 @@ class ContactListAdapter(
}
}
- override fun getItemCount(): Int {
- return items.size
- }
+ override fun getItemCount(): Int { return items.size }
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
- if (holder is ContactViewHolder) {
- holder.unbind()
- }
+ if (holder is ContactViewHolder) { holder.unbind() }
}
override fun getItemViewType(position: Int): Int {
@@ -73,13 +85,9 @@ class ContactListAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == ViewType.Contact) {
- ContactViewHolder(
- ViewContactBinding.inflate(LayoutInflater.from(context), parent, false)
- )
+ ContactViewHolder(ViewContactBinding.inflate(LayoutInflater.from(context), parent, false))
} else {
- HeaderViewHolder(
- ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false)
- )
+ HeaderViewHolder(ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false))
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationHomeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationHomeFragment.kt
index 2e62932ab0..92f050f76a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationHomeFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationHomeFragment.kt
@@ -55,7 +55,7 @@ class NewConversationHomeFragment : Fragment() {
val displayName = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionId
ContactListItem.Contact(it, displayName)
}.sortedBy { it.displayName }
- .groupBy { if (PublicKeyValidation.isValid(it.displayName)) unknownSectionTitle else it.displayName.first().uppercase() }
+ .groupBy { if (PublicKeyValidation.isValid(it.displayName)) unknownSectionTitle else it.displayName.firstOrNull()?.uppercase() ?: unknownSectionTitle }
.toMutableMap()
contactGroups.remove(unknownSectionTitle)?.let { contactGroups.put(unknownSectionTitle, it) }
adapter.items = contactGroups.flatMap { entry -> listOf(ContactListItem.Header(entry.key)) + entry.value }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index a819c3fa22..187ded770e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -6,7 +6,6 @@ import android.animation.ValueAnimator
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
-import android.content.DialogInterface
import android.content.Intent
import android.content.res.Resources
import android.database.Cursor
@@ -19,7 +18,10 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
+import android.text.SpannableStringBuilder
+import android.text.SpannedString
import android.text.TextUtils
+import android.text.style.StyleSpan
import android.util.Pair
import android.util.TypedValue
import android.view.ActionMode
@@ -28,16 +30,21 @@ import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
-import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import androidx.annotation.DimenRes
-import androidx.appcompat.app.AlertDialog
+import androidx.core.text.set
+import androidx.core.text.toSpannable
import androidx.core.view.drawToBitmap
+import androidx.core.view.isGone
import androidx.core.view.isVisible
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
@@ -46,28 +53,36 @@ import androidx.recyclerview.widget.RecyclerView
import com.annimon.stream.Stream
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityConversationV2Binding
-import network.loki.messenger.databinding.ViewVisibleMessageBinding
+import network.loki.messenger.libsession_util.util.ExpiryMode
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
+import org.session.libsession.messaging.jobs.AttachmentDownloadJob
+import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.mentions.MentionsManager
+import org.session.libsession.messaging.messages.ExpirationConfiguration
+import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.control.DataExtractionNotification
-import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.Reaction
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi
-import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
+import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil
@@ -84,14 +99,19 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPrivateKey
import org.thoughtcrime.securesms.ApplicationContext
-import org.thoughtcrime.securesms.ExpirationDialog
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
import org.thoughtcrime.securesms.audio.AudioRecorder
+import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
-import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
+import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
+import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
@@ -107,20 +127,16 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDel
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
-import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
import org.thoughtcrime.securesms.database.GroupDatabase
-import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.ReactionDatabase
-import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.Storage
@@ -148,15 +164,22 @@ import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
+import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask
+import org.thoughtcrime.securesms.util.SimpleTextWatcher
+import org.thoughtcrime.securesms.util.isScrolledToBottom
+import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push
+import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.toPx
+import java.lang.ref.WeakReference
import java.util.Locale
import java.util.concurrent.ExecutionException
+import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
@@ -165,6 +188,8 @@ import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
+private const val TAG = "ConversationActivityV2"
+
// Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
// part of the conversation activity layout. This is just because it makes the layout a lot simpler. The
// price we pay is a bit of back and forth between the input bar and the conversation activity.
@@ -172,7 +197,7 @@ import kotlin.math.sqrt
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate, VisibleMessageViewDelegate, RecipientModifiedListener,
- SearchBottomBar.EventListener, LoaderManager.LoaderCallbacks,
+ SearchBottomBar.EventListener, LoaderManager.LoaderCallbacks, ConversationActionBarDelegate,
OnReactionSelectedListener, ReactWithAnyEmojiDialogFragment.Callback, ReactionsDialogFragment.Callback,
ConversationMenuHelper.ConversationMenuListener {
@@ -184,8 +209,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
@Inject lateinit var sessionContactDb: SessionContactDatabase
@Inject lateinit var groupDb: GroupDatabase
- @Inject lateinit var recipientDb: RecipientDatabase
- @Inject lateinit var lokiApiDb: LokiAPIDatabase
@Inject lateinit var smsDb: SmsDatabase
@Inject lateinit var mmsDb: MmsDatabase
@Inject lateinit var lokiMessageDb: LokiMessageDatabase
@@ -216,23 +239,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
fromSerialized(it)
- } ?: run {
- val openGroupInboxId =
- "${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
- fromSerialized(GroupUtil.getEncodedOpenGroupInboxID(openGroupInboxId))
- }
+ } ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, sessionId)
} else {
it
}
val recipient = Recipient.from(this, address, false)
- threadId = threadDb.getOrCreateThreadIdFor(recipient)
+ threadId = storage.getOrCreateThreadIdFor(recipient.address)
}
} ?: finish()
}
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair())
}
private var actionMode: ActionMode? = null
- private var unreadCount = 0
+ private var unreadCount = Int.MAX_VALUE
// Attachments
private val audioRecorder = AudioRecorder(this)
private val stopAudioHandler = Handler(Looper.getMainLooper())
@@ -250,11 +269,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val searchViewModel: SearchViewModel by viewModels()
var searchViewItem: MenuItem? = null
+ private val bufferedLastSeenChannel = Channel(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+ private var emojiPickerVisible = false
+
private val isScrolledToBottom: Boolean
- get() {
- val position = layoutManager?.findFirstCompletelyVisibleItemPosition() ?: 0
- return position == 0
- }
+ get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
+
+ private val isScrolledToWithin30dpOfBottom: Boolean
+ get() = binding?.conversationRecyclerView?.isScrolledToWithin30dpOfBottom ?: true
private val layoutManager: LinearLayoutManager?
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
@@ -264,17 +286,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (hexEncodedSeed == null) {
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
}
+
+ val appContext = applicationContext
val loadFileContents: (String) -> String = { fileName ->
- MnemonicUtilities.loadFileContents(this, fileName)
+ MnemonicUtilities.loadFileContents(appContext, fileName)
}
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
}
+ // There is a bug when initially joining a community where all messages will immediately be marked
+ // as read if we reverse the message list so this is now hard-coded to false
+ private val reverseMessageList = false
+
private val adapter by lazy {
- val cursor = mmsSmsDb.getConversation(viewModel.threadId, !isIncomingMessageRequestThread())
+ val cursor = mmsSmsDb.getConversation(viewModel.threadId, reverseMessageList)
val adapter = ConversationAdapter(
this,
cursor,
+ storage.getLastSeen(viewModel.threadId),
+ reverseMessageList,
onItemPress = { message, position, view, event ->
handlePress(message, position, view, event)
},
@@ -282,8 +312,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
handleSwipeToReply(message)
},
onItemLongPress = { message, position, view ->
- if (!isMessageRequestThread() &&
- (viewModel.openGroup == null || Capability.REACTIONS.name.lowercase() in viewModel.serverCapabilities)
+ if (!viewModel.isMessageRequestThread &&
+ viewModel.canReactToMessages
) {
showEmojiPicker(message, view)
} else {
@@ -295,10 +325,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
onDeselect(message, position, it)
}
},
+ onAttachmentNeedsDownload = { attachmentId, mmsId ->
+ lifecycleScope.launch(Dispatchers.IO) {
+ JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
+ }
+ },
glide = glide,
lifecycleCoroutineScope = lifecycleScope
)
adapter.visibleMessageViewDelegate = this
+
+ // Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView for if
+ // we're already near the the bottom and the data changes.
+ adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter))
+
adapter
}
@@ -310,10 +350,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val cameraButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_camera_24, hasOpaqueBackground = true) }
private val messageToScrollTimestamp = AtomicLong(-1)
private val messageToScrollAuthor = AtomicReference(null)
+ private val firstLoad = AtomicBoolean(true)
private lateinit var reactionDelegate: ConversationReactionDelegate
private val reactWithAnyEmojiStartPage = -1
+ // Properties for what message indices are visible previously & now, as well as the scroll state
+ private var previousLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
+ private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
+ private var recyclerScrollState: Int = RecyclerView.SCROLL_STATE_IDLE
+
// region Settings
companion object {
// Extras
@@ -328,7 +374,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
const val PICK_GIF = 10
const val PICK_FROM_LIBRARY = 12
const val INVITE_CONTACTS = 124
-
}
// endregion
@@ -337,44 +382,86 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
super.onCreate(savedInstanceState, isReady)
binding = ActivityConversationV2Binding.inflate(layoutInflater)
setContentView(binding!!.root)
+
// messageIdToScroll
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
- val thread = threadDb.getRecipientForThreadId(viewModel.threadId)
- if (thread == null) {
+ val recipient = viewModel.recipient
+ val openGroup = recipient.let { viewModel.openGroup }
+ if (recipient == null || (recipient.isCommunityRecipient && openGroup == null)) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
}
- setUpRecyclerView()
+
setUpToolBar()
setUpInputBar()
setUpLinkPreviewObserver()
restoreDraftIfNeeded()
setUpUiStateObserver()
+
binding!!.scrollToBottomButton.setOnClickListener {
- val layoutManager = binding?.conversationRecyclerView?.layoutManager ?: return@setOnClickListener
+ val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
+ val targetPosition = if (reverseMessageList) 0 else adapter.itemCount
+
if (layoutManager.isSmoothScrolling) {
- binding?.conversationRecyclerView?.scrollToPosition(0)
+ binding?.conversationRecyclerView?.scrollToPosition(targetPosition)
} else {
- binding?.conversationRecyclerView?.smoothScrollToPosition(0)
+ // It looks like 'smoothScrollToPosition' will actually load all intermediate items in
+ // order to do the scroll, this can be very slow if there are a lot of messages so
+ // instead we check the current position and if there are more than 10 items to scroll
+ // we jump instantly to the 10th item and scroll from there (this should happen quick
+ // enough to give a similar scroll effect without having to load everything)
+// val position = if (reverseMessageList) layoutManager.findFirstVisibleItemPosition() else layoutManager.findLastVisibleItemPosition()
+// val targetBuffer = if (reverseMessageList) 10 else Math.max(0, (adapter.itemCount - 1) - 10)
+// if (position > targetBuffer) {
+// binding?.conversationRecyclerView?.scrollToPosition(targetBuffer)
+// }
+
+ binding?.conversationRecyclerView?.post {
+ binding?.conversationRecyclerView?.smoothScrollToPosition(targetPosition)
+ }
}
}
- unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
+
updateUnreadCountIndicator()
- setUpTypingObserver()
- setUpRecipientObserver()
- updateSubtitle()
- getLatestOpenGroupInfoIfNeeded()
+ updatePlaceholder()
setUpBlockedBanner()
binding!!.searchBottomBar.setEventListener(this)
- setUpSearchResultObserver()
- scrollToFirstUnreadMessageIfNeeded()
- showOrHideInputIfNeeded()
+ updateSendAfterApprovalText()
setUpMessageRequestsBar()
- viewModel.recipient?.let { recipient ->
- if (recipient.isOpenGroupRecipient && viewModel.openGroup == null) {
- Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
- return finish()
+
+ // Note: Do not `showOrHideInputIfNeeded` here - we'll never start this activity w/ the
+ // keyboard visible and have no need to immediately display it.
+
+ val weakActivity = WeakReference(this)
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ // Note: We are accessing the `adapter` property because we want it to be loaded on
+ // the background thread to avoid blocking the UI thread and potentially hanging when
+ // transitioning to the activity
+ weakActivity.get()?.adapter ?: return@launch
+
+ // 'Get' instead of 'GetAndSet' here because we want to trigger the highlight in 'onFirstLoad'
+ // by triggering 'jumpToMessage' using these values
+ val messageTimestamp = messageToScrollTimestamp.get()
+ val author = messageToScrollAuthor.get()
+ val targetPosition = if (author != null && messageTimestamp >= 0) mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, messageTimestamp, author, reverseMessageList) else -1
+
+ withContext(Dispatchers.Main) {
+ setUpRecyclerView()
+ setUpTypingObserver()
+ setUpRecipientObserver()
+ getLatestOpenGroupInfoIfNeeded()
+ setUpSearchResultObserver()
+ scrollToFirstUnreadMessageIfNeeded()
+ setUpOutdatedClientBanner()
+
+ if (author != null && messageTimestamp >= 0 && targetPosition >= 0) {
+ binding?.conversationRecyclerView?.scrollToPosition(targetPosition)
+ }
+ else {
+ scrollToFirstUnreadMessageIfNeeded(true)
+ }
}
}
@@ -382,18 +469,37 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
ViewUtil.findStubById(this, R.id.conversation_reaction_scrubber_stub)
reactionDelegate = ConversationReactionDelegate(reactionOverlayStub)
reactionDelegate.setOnReactionSelectedListener(this)
+ lifecycleScope.launch {
+ // only update the conversation every 3 seconds maximum
+ // channel is rendezvous and shouldn't block on try send calls as often as we want
+ bufferedLastSeenChannel.receiveAsFlow()
+ .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
+ .collectLatest {
+ withContext(Dispatchers.IO) {
+ try {
+ if (it > storage.getLastSeen(viewModel.threadId)) {
+ storage.markConversationAsRead(viewModel.threadId, it)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "bufferedLastSeenChannel collectLatest", e)
+ }
+ }
+ }
+ }
}
override fun onResume() {
super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
- val recipient = viewModel.recipient ?: return
- threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
+
contentResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
true,
screenshotObserver
)
+ viewModel.run {
+ binding?.toolbarContent?.update(recipient ?: return, openGroup, expirationConfiguration)
+ }
}
override fun onPause() {
@@ -410,26 +516,47 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun dispatchIntent(body: (Context) -> Intent?) {
- val intent = body(this) ?: return
- push(intent, false)
+ body(this)?.let { push(it, false) }
}
- override fun showDialog(baseDialog: BaseDialog, tag: String?) {
- baseDialog.show(supportFragmentManager, tag)
+ override fun showDialog(dialogFragment: DialogFragment, tag: String?) {
+ dialogFragment.show(supportFragmentManager, tag)
}
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader {
- return ConversationLoader(viewModel.threadId, !isIncomingMessageRequestThread(), this@ConversationActivityV2)
+ return ConversationLoader(viewModel.threadId, reverseMessageList, this@ConversationActivityV2)
}
override fun onLoadFinished(loader: Loader, cursor: Cursor?) {
+ val oldCount = adapter.itemCount
+ val newCount = cursor?.count ?: 0
adapter.changeCursor(cursor)
+
if (cursor != null) {
val messageTimestamp = messageToScrollTimestamp.getAndSet(-1)
val author = messageToScrollAuthor.getAndSet(null)
- if (author != null && messageTimestamp >= 0) {
- jumpToMessage(author, messageTimestamp, null)
+ val initialUnreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
+
+ // Update the unreadCount value to be loaded from the database since we got a new message
+ if (firstLoad.get() || oldCount != newCount || initialUnreadCount != unreadCount) {
+ // Update the unreadCount value to be loaded from the database since we got a new
+ // message (we need to store it in a local variable as it can get overwritten on
+ // another thread before the 'firstLoad.getAndSet(false)' case below)
+ unreadCount = initialUnreadCount
+ updateUnreadCountIndicator()
}
+
+ if (author != null && messageTimestamp >= 0) {
+ jumpToMessage(author, messageTimestamp, firstLoad.get(), null)
+ } else {
+ if (firstLoad.getAndSet(false)) scrollToFirstUnreadMessageIfNeeded(true)
+ handleRecyclerViewScrolled()
+ }
+ }
+ updatePlaceholder()
+ viewModel.recipient?.let {
+ maybeUpdateToolbar(recipient = it)
+ setUpOutdatedClientBanner()
}
}
@@ -440,66 +567,97 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate
private fun setUpRecyclerView() {
binding!!.conversationRecyclerView.adapter = adapter
- val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, !isIncomingMessageRequestThread())
+ val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, reverseMessageList)
binding!!.conversationRecyclerView.layoutManager = layoutManager
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, this)
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ // The unreadCount check is to prevent us scrolling to the bottom when we first enter a conversation
+ if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE && unreadCount != Int.MAX_VALUE) {
+ scrollToMostRecentMessageIfWeShould()
+ }
handleRecyclerViewScrolled()
}
+
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ recyclerScrollState = newState
+ }
})
}
+ private fun scrollToMostRecentMessageIfWeShould() {
+ // Grab an initial 'previous' last visible message..
+ if (previousLastVisibleRecyclerViewIndex == RecyclerView.NO_POSITION) {
+ previousLastVisibleRecyclerViewIndex = layoutManager?.findLastVisibleItemPosition()!!
+ }
+
+ // ..and grab the 'current' last visible message.
+ currentLastVisibleRecyclerViewIndex = layoutManager?.findLastVisibleItemPosition()!!
+
+ // If the current last visible message index is less than the previous one (i.e. we've
+ // lost visibility of one or more messages due to showing the IME keyboard) AND we're
+ // at the bottom of the message feed..
+ val atBottomAndTrueLastNoLongerVisible = currentLastVisibleRecyclerViewIndex!! <= previousLastVisibleRecyclerViewIndex!! && !binding?.scrollToBottomButton?.isVisible!!
+
+ // ..OR we're at the last message or have received a new message..
+ val atLastOrReceivedNewMessage = currentLastVisibleRecyclerViewIndex == (adapter.itemCount - 1)
+
+ // ..then scroll the recycler view to the last message on resize. Note: We cannot just call
+ // scroll/smoothScroll - we have to `post` it or nothing happens!
+ if (atBottomAndTrueLastNoLongerVisible || atLastOrReceivedNewMessage) {
+ binding?.conversationRecyclerView?.post {
+ binding?.conversationRecyclerView?.smoothScrollToPosition(adapter.itemCount)
+ }
+ }
+
+ // Update our previous last visible view index to the current one
+ previousLastVisibleRecyclerViewIndex = currentLastVisibleRecyclerViewIndex
+ }
+
// called from onCreate
private fun setUpToolBar() {
- setSupportActionBar(binding?.toolbar)
+ val binding = binding ?: return
+ setSupportActionBar(binding.toolbar)
val actionBar = supportActionBar ?: return
val recipient = viewModel.recipient ?: return
actionBar.title = ""
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setHomeButtonEnabled(true)
- binding!!.toolbarContent.conversationTitleView.text = when {
- recipient.isLocalNumber -> getString(R.string.note_to_self)
- else -> recipient.toShortString()
- }
- @DimenRes val sizeID: Int = if (viewModel.recipient?.isClosedGroupRecipient == true) {
- R.dimen.medium_profile_picture_size
- } else {
- R.dimen.small_profile_picture_size
- }
- val size = resources.getDimension(sizeID).roundToInt()
- binding!!.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
- binding!!.toolbarContent.profilePictureView.root.glide = glide
- MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
- val profilePictureView = binding!!.toolbarContent.profilePictureView.root
- viewModel.recipient?.let { recipient ->
- profilePictureView.update(recipient)
- }
+ binding!!.toolbarContent.bind(
+ this,
+ viewModel.threadId,
+ recipient,
+ viewModel.expirationConfiguration,
+ viewModel.openGroup
+ )
+ maybeUpdateToolbar(recipient)
}
// called from onCreate
private fun setUpInputBar() {
- binding!!.inputBar.delegate = this
- binding!!.inputBarRecordingView.delegate = this
+ val binding = binding ?: return
+ binding.inputBar.isGone = viewModel.hidesInputBar()
+ binding.inputBar.delegate = this
+ binding.inputBarRecordingView.delegate = this
// GIF button
- binding!!.gifButtonContainer.addView(gifButton)
+ binding.gifButtonContainer.addView(gifButton)
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
gifButton.onUp = { showGIFPicker() }
gifButton.snIsEnabled = false
// Document button
- binding!!.documentButtonContainer.addView(documentButton)
+ binding.documentButtonContainer.addView(documentButton)
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
documentButton.onUp = { showDocumentPicker() }
documentButton.snIsEnabled = false
// Library button
- binding!!.libraryButtonContainer.addView(libraryButton)
+ binding.libraryButtonContainer.addView(libraryButton)
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
libraryButton.onUp = { pickFromLibrary() }
libraryButton.snIsEnabled = false
// Camera button
- binding!!.cameraButtonContainer.addView(cameraButton)
+ binding.cameraButtonContainer.addView(cameraButton)
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
cameraButton.onUp = { showCamera() }
cameraButton.snIsEnabled = false
@@ -566,23 +724,36 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun getLatestOpenGroupInfoIfNeeded() {
- viewModel.openGroup?.let {
- OpenGroupApi.getMemberCount(it.room, it.server).successUi { updateSubtitle() }
+ val openGroup = viewModel.openGroup ?: return
+ OpenGroupApi.getMemberCount(openGroup.room, openGroup.server) successUi {
+ binding?.toolbarContent?.updateSubtitle(viewModel.recipient!!, openGroup, viewModel.expirationConfiguration)
+ maybeUpdateToolbar(viewModel.recipient!!)
}
}
// called from onCreate
private fun setUpBlockedBanner() {
- val recipient = viewModel.recipient ?: return
- if (recipient.isGroupRecipient) { return }
+ val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
val sessionID = recipient.address.toString()
- val contact = sessionContactDb.getContactWithSessionID(sessionID)
- val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
+ val name = sessionContactDb.getContactWithSessionID(sessionID)?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
binding?.blockedBanner?.isVisible = recipient.isBlocked
binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
}
+ private fun setUpOutdatedClientBanner() {
+ val legacyRecipient = viewModel.legacyBannerRecipient(this)
+
+ val shouldShowLegacy = ExpirationConfiguration.isNewConfigEnabled &&
+ legacyRecipient != null
+
+ binding?.outdatedBanner?.isVisible = shouldShowLegacy
+ if (shouldShowLegacy) {
+ binding?.outdatedBannerTextView?.text =
+ resources.getString(R.string.activity_conversation_outdated_client_banner_text, legacyRecipient!!.name)
+ }
+ }
+
private fun setUpLinkPreviewObserver() {
if (!textSecurePreferences.isLinkPreviewsEnabled()) {
linkPreviewViewModel.onUserCancel(); return
@@ -613,43 +784,65 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (uiState.isMessageRequestAccepted == true) {
binding?.messageRequestBar?.visibility = View.GONE
}
+ if (!uiState.conversationExists && !isFinishing) {
+ // Conversation should be deleted now, just go back
+ finish()
+ }
}
}
}
- private fun scrollToFirstUnreadMessageIfNeeded() {
+ private fun scrollToFirstUnreadMessageIfNeeded(isFirstLoad: Boolean = false, shouldHighlight: Boolean = false): Int {
val lastSeenTimestamp = threadDb.getLastSeenAndHasSent(viewModel.threadId).first()
- val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return
- if (lastSeenItemPosition <= 3) { return }
+ val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return -1
+
+ // If this is triggered when first opening a conversation then we want to position the top
+ // of the first unread message in the middle of the screen
+ if (isFirstLoad && !reverseMessageList) {
+ layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
+ if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
+ return lastSeenItemPosition
+ }
+
+ if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
+
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
+ return lastSeenItemPosition
+ }
+
+ private fun highlightViewAtPosition(position: Int) {
+ binding?.conversationRecyclerView?.post {
+ (layoutManager?.findViewByPosition(position) as? VisibleMessageView)?.playHighlight()
+ }
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val recipient = viewModel.recipient ?: return false
- if (!isMessageRequestThread()) {
+ if (!viewModel.isMessageRequestThread) {
ConversationMenuHelper.onPrepareOptionsMenu(
menu,
menuInflater,
recipient,
- viewModel.threadId,
this
- ) { onOptionsItemSelected(it) }
+ )
}
- super.onPrepareOptionsMenu(menu)
+ maybeUpdateToolbar(recipient)
return true
}
override fun onDestroy() {
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
+ cancelVoiceMessage()
tearDownRecipientObserver()
super.onDestroy()
binding = null
-// actionBarBinding = null
}
// endregion
// region Animation & Updating
override fun onModified(recipient: Recipient) {
+ viewModel.updateRecipient()
+
runOnUiThread {
val threadRecipient = viewModel.recipient ?: return@runOnUiThread
if (threadRecipient.isContactRecipient) {
@@ -657,25 +850,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
setUpMessageRequestsBar()
invalidateOptionsMenu()
- updateSubtitle()
+ updateSendAfterApprovalText()
showOrHideInputIfNeeded()
- binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient)
- binding?.toolbarContent?.conversationTitleView?.text = when {
- threadRecipient.isLocalNumber -> getString(R.string.note_to_self)
- else -> threadRecipient.toShortString()
- }
+
+ maybeUpdateToolbar(threadRecipient)
}
}
+ private fun maybeUpdateToolbar(recipient: Recipient) {
+ binding?.toolbarContent?.update(recipient, viewModel.openGroup, viewModel.expirationConfiguration)
+ }
+
+ private fun updateSendAfterApprovalText() {
+ binding?.textSendAfterApproval?.isVisible = viewModel.showSendAfterApprovalText
+ }
+
private fun showOrHideInputIfNeeded() {
- val recipient = viewModel.recipient
- if (recipient != null && recipient.isClosedGroupRecipient) {
- val group = groupDb.getGroup(recipient.address.toGroupString()).orNull()
- val isActive = (group?.isActive == true)
- binding?.inputBar?.showInput = isActive
- } else {
- binding?.inputBar?.showInput = true
- }
+ binding?.inputBar?.showInput = viewModel.recipient?.takeIf { it.isClosedGroupRecipient }
+ ?.run { address.toGroupString().let(groupDb::getGroup).orNull()?.isActive == true }
+ ?: true
}
private fun setUpMessageRequestsBar() {
@@ -698,36 +891,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun acceptMessageRequest() {
binding?.messageRequestBar?.isVisible = false
- binding?.conversationRecyclerView?.layoutManager =
- LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
- adapter.notifyDataSetChanged()
viewModel.acceptMessageRequest()
- LoaderManager.getInstance(this).restartLoader(0, null, this)
+
lifecycleScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ConversationActivityV2)
}
}
- private fun isMessageRequestThread(): Boolean {
- val recipient = viewModel.recipient ?: return false
- return !recipient.isGroupRecipient && !recipient.isApproved
- }
+ private fun isOutgoingMessageRequestThread(): Boolean = viewModel.recipient?.run {
+ !isGroupRecipient && !isLocalNumber &&
+ !(hasApprovedMe() || viewModel.hasReceived())
+ } ?: false
- private fun isOutgoingMessageRequestThread(): Boolean {
- val recipient = viewModel.recipient ?: return false
- return !recipient.isGroupRecipient &&
- !recipient.isLocalNumber &&
- !(recipient.hasApprovedMe() || viewModel.hasReceived())
- }
-
- private fun isIncomingMessageRequestThread(): Boolean {
- val recipient = viewModel.recipient ?: return false
- return !recipient.isGroupRecipient &&
- !recipient.isApproved &&
- !recipient.isLocalNumber &&
- !threadDb.getLastSeenAndHasSent(viewModel.threadId).second() &&
- threadDb.getMessageCount(viewModel.threadId) > 0
- }
+ private fun isIncomingMessageRequestThread(): Boolean = viewModel.recipient?.run {
+ !isGroupRecipient && !isApproved && !isLocalNumber &&
+ !threadDb.getLastSeenAndHasSent(viewModel.threadId).second() && threadDb.getMessageCount(viewModel.threadId) > 0
+ } ?: false
override fun inputBarEditTextContentChanged(newContent: CharSequence) {
val inputBarText = binding?.inputBar?.text ?: return // TODO check if we should be referencing newContent here instead
@@ -786,15 +965,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val recipient = viewModel.recipient ?: return
if (!isShowingMentionCandidatesView) {
additionalContentContainer.removeAllViews()
- val view = MentionCandidatesView(this)
+ val view = MentionCandidatesView(this).apply {
+ contentDescription = context.getString(R.string.AccessibilityId_mentions_list)
+ }
view.glide = glide
view.onCandidateSelected = { handleMentionSelected(it) }
additionalContentContainer.addView(view)
- val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
+ val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
this.mentionCandidatesView = view
view.show(candidates, viewModel.threadId)
} else {
- val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
+ val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
this.mentionCandidatesView!!.setMentionCandidates(candidates)
}
isShowingMentionCandidatesView = true
@@ -840,7 +1021,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun showVoiceMessageUI() {
- binding?.inputBarRecordingView?.show()
+ binding?.inputBarRecordingView?.show(lifecycleScope)
binding?.inputBar?.alpha = 0.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
animation.duration = 250L
@@ -898,20 +1079,69 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun handleRecyclerViewScrolled() {
- // FIXME: Checking isScrolledToBottom is a quick fix for an issue where the
- // typing indicator overlays the recycler view when scrolled up
val binding = binding ?: return
+
+ // Note: The typing indicate is whether the other person / other people are typing - it has
+ // nothing to do with the IME keyboard state.
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
- binding.typingIndicatorViewContainer.isVisible
- showOrHidScrollToBottomButton()
- val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: -1
- unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0)
+
+ showScrollToBottomButtonIfApplicable()
+ val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition()
+ val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
+ if (!firstLoad.get() && targetVisiblePosition != RecyclerView.NO_POSITION) {
+ adapter.getTimestampForItemAt(targetVisiblePosition)?.let { visibleItemTimestamp ->
+ bufferedLastSeenChannel.trySend(visibleItemTimestamp).apply {
+ if (isFailure) Log.e(TAG, "trySend failed", exceptionOrNull())
+ }
+ }
+ }
+
+ if (reverseMessageList) {
+ unreadCount = min(unreadCount, targetVisiblePosition).coerceAtLeast(0)
+ } else {
+ val layoutUnreadCount = layoutManager?.let { (it.itemCount - 1) - it.findLastVisibleItemPosition() }
+ ?: RecyclerView.NO_POSITION
+ unreadCount = min(unreadCount, layoutUnreadCount).coerceAtLeast(0)
+ }
updateUnreadCountIndicator()
}
- private fun showOrHidScrollToBottomButton(show: Boolean = true) {
- binding?.scrollToBottomButton?.isVisible = show && !isScrolledToBottom && adapter.itemCount > 0
+ private fun updatePlaceholder() {
+ val recipient = viewModel.recipient
+ ?: return Log.w("Loki", "recipient was null in placeholder update")
+ val blindedRecipient = viewModel.blindedRecipient
+ val binding = binding ?: return
+ val openGroup = viewModel.openGroup
+
+ val (textResource, insertParam) = when {
+ recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
+ openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
+ blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString()
+ else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
+ }
+ val showPlaceholder = adapter.itemCount == 0
+ binding.placeholderText.isVisible = showPlaceholder
+ if (showPlaceholder) {
+ if (insertParam != null) {
+ val span = getText(textResource) as SpannedString
+ val annotations = span.getSpans(0, span.length, StyleSpan::class.java)
+ val boldSpan = annotations.first()
+ val spannedParam = insertParam.toSpannable()
+ spannedParam[0 until spannedParam.length] = StyleSpan(boldSpan.style)
+ val originalStart = span.getSpanStart(boldSpan)
+ val originalEnd = span.getSpanEnd(boldSpan)
+ val newString = SpannableStringBuilder(span)
+ .replace(originalStart, originalEnd, spannedParam)
+ binding.placeholderText.text = newString
+ } else {
+ binding.placeholderText.setText(textResource)
+ }
+ }
+ }
+
+ private fun showScrollToBottomButtonIfApplicable() {
+ binding?.scrollToBottomButton?.isVisible = !emojiPickerVisible && !isScrolledToBottom && adapter.itemCount > 0
}
private fun updateUnreadCountIndicator() {
@@ -924,33 +1154,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.unreadCountIndicator.isVisible = (unreadCount != 0)
}
- private fun updateSubtitle() {
- val actionBarBinding = binding?.toolbarContent ?: return
- val recipient = viewModel.recipient ?: return
- actionBarBinding.muteIconImageView.isVisible = recipient.isMuted
- actionBarBinding.conversationSubtitleView.isVisible = true
- if (recipient.isMuted) {
- if (recipient.mutedUntil != Long.MAX_VALUE) {
- actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
- } else {
- actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever)
- }
- } else if (recipient.isGroupRecipient) {
- viewModel.openGroup?.let { openGroup ->
- val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0
- actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_active_member_count, userCount)
- } ?: run {
- val userCount = groupDb.getGroupMemberAddresses(recipient.address.toGroupString(), true).size
- actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_member_count, userCount)
- }
- viewModel
- } else {
- actionBarBinding.conversationSubtitleView.isVisible = false
- }
- }
// endregion
// region Interaction
+ override fun onDisappearingMessagesClicked() {
+ viewModel.recipient?.let { showDisappearingMessages(it) }
+ }
+
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
return false
@@ -961,19 +1171,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun block(deleteThread: Boolean) {
- val title = R.string.RecipientPreferenceActivity_block_this_contact_question
- val message = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact
- AlertDialog.Builder(this)
- .setTitle(title)
- .setMessage(message)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.RecipientPreferenceActivity_block) { _, _ ->
+ showSessionDialog {
+ title(R.string.RecipientPreferenceActivity_block_this_contact_question)
+ text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
+ destructiveButton(R.string.RecipientPreferenceActivity_block, R.string.AccessibilityId_block_confirm) {
viewModel.block()
if (deleteThread) {
viewModel.deleteThread()
finish()
}
- }.show()
+ }
+ cancelButton()
+ }
}
override fun copySessionID(sessionId: String) {
@@ -983,33 +1192,37 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
- override fun showExpiringMessagesDialog(thread: Recipient) {
+ override fun copyOpenGroupUrl(thread: Recipient) {
+ if (!thread.isCommunityRecipient) { return }
+
+ val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return
+ val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
+
+ val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
+ val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
+ manager.setPrimaryClip(clip)
+ Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
+ }
+
+ override fun showDisappearingMessages(thread: Recipient) {
if (thread.isClosedGroupRecipient) {
- val group = groupDb.getGroup(thread.address.toGroupString()).orNull()
- if (group?.isActive == false) { return }
- }
- ExpirationDialog.show(this, thread.expireMessages) { expirationTime: Int ->
- recipientDb.setExpireMessages(thread, expirationTime)
- val message = ExpirationTimerUpdate(expirationTime)
- message.recipient = thread.address.serialize()
- message.sentTimestamp = System.currentTimeMillis()
- val expiringMessageManager = ApplicationContext.getInstance(this).expiringMessageManager
- expiringMessageManager.setExpirationTimer(message)
- MessageSender.send(message, thread.address)
- invalidateOptionsMenu()
+ groupDb.getGroup(thread.address.toGroupString()).orNull()?.run { if (!isActive) return }
}
+ Intent(this, DisappearingMessagesActivity::class.java)
+ .apply { putExtra(DisappearingMessagesActivity.THREAD_ID, viewModel.threadId) }
+ .also { show(it, true) }
}
override fun unblock() {
- val title = R.string.ConversationActivity_unblock_this_contact_question
- val message = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact
- AlertDialog.Builder(this)
- .setTitle(title)
- .setMessage(message)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.ConversationActivity_unblock) { _, _ ->
- viewModel.unblock()
- }.show()
+ showSessionDialog {
+ title(R.string.ConversationActivity_unblock_this_contact_question)
+ text(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
+ destructiveButton(
+ R.string.ConversationActivity_unblock,
+ R.string.AccessibilityId_block_confirm
+ ) { viewModel.unblock() }
+ cancelButton()
+ }
}
// `position` is the adapter position; not the visual position
@@ -1039,6 +1252,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord) {
+ if (message.isOpenGroupInvitation) return
val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, message, glide)
}
@@ -1069,33 +1283,37 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
Log.e("Loki", "Failed to show emoji picker", e)
return
}
+
+ val binding = binding ?: return
+
+ emojiPickerVisible = true
ViewUtil.hideKeyboard(this, visibleMessageView)
- binding?.reactionsShade?.isVisible = true
- showOrHidScrollToBottomButton(false)
- binding?.conversationRecyclerView?.suppressLayout(true)
+ binding.reactionsShade.isVisible = true
+ binding.scrollToBottomButton.isVisible = false
+ binding.conversationRecyclerView.suppressLayout(true)
reactionDelegate.setOnActionSelectedListener(ReactionsToolbarListener(message))
reactionDelegate.setOnHideListener(object: ConversationReactionOverlay.OnHideListener {
override fun startHide() {
- binding?.reactionsShade?.let {
+ emojiPickerVisible = false
+ binding.reactionsShade.let {
ViewUtil.fadeOut(it, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE)
}
- showOrHidScrollToBottomButton(true)
+ showScrollToBottomButtonIfApplicable()
}
override fun onHide() {
- binding?.conversationRecyclerView?.suppressLayout(false)
+ binding.conversationRecyclerView.suppressLayout(false)
WindowUtil.setLightStatusBarFromTheme(this@ConversationActivityV2);
WindowUtil.setLightNavigationBarFromTheme(this@ConversationActivityV2);
}
})
- val contentBounds = Rect()
- visibleMessageView.messageContentView.getGlobalVisibleRect(contentBounds)
+ val topLeft = intArrayOf(0, 0).also { visibleMessageView.messageContentView.getLocationInWindow(it) }
val selectedConversationModel = SelectedConversationModel(
messageContentBitmap,
- contentBounds.left.toFloat(),
- contentBounds.top.toFloat(),
+ topLeft[0].toFloat(),
+ topLeft[1].toFloat(),
visibleMessageView.messageContentView.width,
message.isOutgoing,
visibleMessageView.messageContentView
@@ -1114,6 +1332,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendEmojiRemoval(emoji, messageRecord)
} else {
sendEmojiReaction(emoji, messageRecord)
+ RecentEmojiPageModel.onCodePointSelected(emoji) // Save to recently used reaction emojis
}
}
@@ -1121,7 +1340,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Create the message
val recipient = viewModel.recipient ?: return
val reactionMessage = VisibleMessage()
- val emojiTimestamp = System.currentTimeMillis()
+ val emojiTimestamp = SnodeAPI.nowWithOffset
reactionMessage.sentTimestamp = emojiTimestamp
val author = textSecurePreferences.getLocalNumber()!!
// Put the message in the database
@@ -1140,7 +1359,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else originalMessage.individualRecipient.address
// Send it
reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, true)
- if (recipient.isOpenGroupRecipient) {
+ if (recipient.isCommunityRecipient) {
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
viewModel.openGroup?.let {
OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji)
@@ -1154,7 +1373,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun sendEmojiRemoval(emoji: String, originalMessage: MessageRecord) {
val recipient = viewModel.recipient ?: return
val message = VisibleMessage()
- val emojiTimestamp = System.currentTimeMillis()
+ val emojiTimestamp = SnodeAPI.nowWithOffset
message.sentTimestamp = emojiTimestamp
val author = textSecurePreferences.getLocalNumber()!!
reactionDb.deleteReaction(emoji, MessageId(originalMessage.id, originalMessage.isMms), author, false)
@@ -1164,7 +1383,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else originalMessage.individualRecipient.address
message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, false)
- if (recipient.isOpenGroupRecipient) {
+ if (recipient.isCommunityRecipient) {
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
viewModel.openGroup?.let {
OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji)
@@ -1338,22 +1557,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return }
val viewHolder = binding?.conversationRecyclerView?.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder ?: return
- val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
- visibleMessageView.playVoiceMessage()
+ viewHolder.view.playVoiceMessage()
}
override fun sendMessage() {
val recipient = viewModel.recipient ?: return
if (recipient.isContactRecipient && recipient.isBlocked) {
- BlockedDialog(recipient).show(supportFragmentManager, "Blocked Dialog")
+ BlockedDialog(recipient, this).show(supportFragmentManager, "Blocked Dialog")
return
}
val binding = binding ?: return
- if (binding.inputBar.linkPreview != null || binding.inputBar.quote != null) {
+ val sentMessageInfo = if (binding.inputBar.linkPreview != null || binding.inputBar.quote != null) {
sendAttachments(listOf(), getMessageBody(), binding.inputBar.quote, binding.inputBar.linkPreview)
} else {
sendTextOnlyMessage()
}
+
+ // Jump to the newly sent message once it gets added
+ if (sentMessageInfo != null) {
+ messageToScrollAuthor.set(sentMessageInfo.first)
+ messageToScrollTimestamp.set(sentMessageInfo.second)
+ }
}
override fun commitInputContent(contentUri: Uri) {
@@ -1371,21 +1595,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
- private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
- val recipient = viewModel.recipient ?: return
+ private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false): Pair? {
+ val recipient = viewModel.recipient ?: return null
+ val sentTimestamp = SnodeAPI.nowWithOffset
processMessageRequestApproval()
val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
- return dialog.show(supportFragmentManager, "Send Seed Dialog")
+ dialog.show(supportFragmentManager, "Send Seed Dialog")
+ return null
}
// Create the message
- val message = VisibleMessage()
- message.sentTimestamp = System.currentTimeMillis()
+ val message = VisibleMessage().applyExpiryMode(viewModel.threadId)
+ message.sentTimestamp = sentTimestamp
message.text = text
- val outgoingTextMessage = OutgoingTextMessage.from(message, recipient)
+ val expiresInMillis = viewModel.expirationConfiguration?.expiryMode?.expiryMillis ?: 0
+ val expireStartedAt = if (viewModel.expirationConfiguration?.expiryMode is ExpiryMode.AfterSend) {
+ message.sentTimestamp!!
+ } else 0
+ val outgoingTextMessage = OutgoingTextMessage.from(message, recipient, expiresInMillis, expireStartedAt)
// Clear the input bar
binding?.inputBar?.text = ""
binding?.inputBar?.cancelQuoteDraft()
@@ -1400,14 +1630,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
MessageSender.send(message, recipient.address)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
+ return Pair(recipient.address, sentTimestamp)
}
- private fun sendAttachments(attachments: List, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
- val recipient = viewModel.recipient ?: return
+ private fun sendAttachments(
+ attachments: List,
+ body: String?,
+ quotedMessage: MessageRecord? = binding?.inputBar?.quote,
+ linkPreview: LinkPreview? = null
+ ): Pair? {
+ val recipient = viewModel.recipient ?: return null
+ val sentTimestamp = SnodeAPI.nowWithOffset
processMessageRequestApproval()
// Create the message
- val message = VisibleMessage()
- message.sentTimestamp = System.currentTimeMillis()
+ val message = VisibleMessage().applyExpiryMode(viewModel.threadId)
+ message.sentTimestamp = sentTimestamp
message.text = body
val quote = quotedMessage?.let {
val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf()
@@ -1422,7 +1659,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
else it.individualRecipient.address
quote?.copy(author = sender)
}
- val outgoingTextMessage = OutgoingMediaMessage.from(message, recipient, attachments, localQuote, linkPreview)
+ val expiresInMs = viewModel.expirationConfiguration?.expiryMode?.expiryMillis ?: 0
+ val expireStartedAtMs = if (viewModel.expirationConfiguration?.expiryMode is ExpiryMode.AfterSend) {
+ sentTimestamp
+ } else 0
+ val outgoingTextMessage = OutgoingMediaMessage.from(message, recipient, attachments, localQuote, linkPreview, expiresInMs, expireStartedAtMs)
// Clear the input bar
binding?.inputBar?.text = ""
binding?.inputBar?.cancelQuoteDraft()
@@ -1441,28 +1682,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
+ return Pair(recipient.address, sentTimestamp)
}
private fun showGIFPicker() {
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
if (!hasSeenGIFMetaDataWarning) {
- val builder = AlertDialog.Builder(this)
- builder.setTitle("Search GIFs?")
- builder.setMessage("You will not have full metadata protection when sending GIFs.")
- builder.setPositiveButton("OK") { dialog: DialogInterface, _: Int ->
- textSecurePreferences.setHasSeenGIFMetaDataWarning()
- AttachmentManager.selectGif(this, PICK_GIF)
- dialog.dismiss()
+ showSessionDialog {
+ title(R.string.giphy_permission_title)
+ text(R.string.giphy_permission_message)
+ button(R.string.continue_2) {
+ textSecurePreferences.setHasSeenGIFMetaDataWarning()
+ selectGif()
+ }
+ cancelButton()
}
- builder.setNegativeButton(
- "Cancel"
- ) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
- builder.create().show()
} else {
- AttachmentManager.selectGif(this, PICK_GIF)
+ selectGif()
}
}
+ private fun selectGif() = AttachmentManager.selectGif(this, PICK_GIF)
+
private fun showDocumentPicker() {
AttachmentManager.selectDocument(this, PICK_DOCUMENT)
}
@@ -1487,6 +1728,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
+ @Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
val mediaPreppedListener = object : ListenableFuture.Listener {
@@ -1537,7 +1779,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendAttachments(slideDeck.asAttachments(), body)
}
INVITE_CONTACTS -> {
- if (viewModel.recipient?.isOpenGroupRecipient != true) { return }
+ if (viewModel.recipient?.isCommunityRecipient != true) { return }
val extras = intent?.extras ?: return
if (!intent.hasExtra(selectedContactsKey)) { return }
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
@@ -1562,7 +1804,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
showVoiceMessageUI()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.startRecording()
- stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each
+ stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 300000) // Limit voice messages to 5 minute each
} else {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
@@ -1603,41 +1845,71 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
handleLongPress(messages.first(), 0) //TODO: begin selection mode
}
+ // The option to "Delete just for me" or "Delete for everyone"
+ private fun showDeleteOrDeleteForEveryoneInCommunityUI(messages: Set) {
+ val bottomSheet = DeleteOptionsBottomSheet()
+ bottomSheet.recipient = viewModel.recipient!!
+ bottomSheet.onDeleteForMeTapped = {
+ messages.forEach(viewModel::deleteLocally)
+ bottomSheet.dismiss()
+ endActionMode()
+ }
+ bottomSheet.onDeleteForEveryoneTapped = {
+ messages.forEach(viewModel::deleteForEveryone)
+ bottomSheet.dismiss()
+ endActionMode()
+ }
+ bottomSheet.onCancelTapped = {
+ bottomSheet.dismiss()
+ endActionMode()
+ }
+ bottomSheet.show(supportFragmentManager, bottomSheet.tag)
+ }
+
+ private fun showDeleteLocallyUI(messages: Set) {
+ val messageCount = 1
+ showSessionDialog {
+ title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
+ text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
+ button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
+ cancelButton(::endActionMode)
+ }
+ }
+
+ // Note: The messages in the provided set may be a single message, or multiple if there are a
+ // group of selected messages.
override fun deleteMessages(messages: Set) {
- val recipient = viewModel.recipient ?: return
+ val recipient = viewModel.recipient
+ if (recipient == null) {
+ Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.")
+ return
+ }
+
val allSentByCurrentUser = messages.all { it.isOutgoing }
- val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null }
- if (recipient.isOpenGroupRecipient) {
- val messageCount = 1
- val builder = AlertDialog.Builder(this)
- builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
- builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
- builder.setCancelable(true)
- builder.setPositiveButton(R.string.delete) { _, _ ->
- for (message in messages) {
- viewModel.deleteForEveryone(message)
+ val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
+
+ // If the recipient is a community OR a Note-to-Self then we delete the message for everyone
+ if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
+ val messageCount = 1 // Only used for plurals string
+ showSessionDialog {
+ title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
+ text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
+ button(R.string.delete) {
+ messages.forEach(viewModel::deleteForEveryone); endActionMode()
}
- endActionMode()
+ cancelButton { endActionMode() }
}
- builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
- endActionMode()
- }
- builder.show()
+ // Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
} else if (allSentByCurrentUser && allHasHash) {
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = recipient
bottomSheet.onDeleteForMeTapped = {
- for (message in messages) {
- viewModel.deleteLocally(message)
- }
+ messages.forEach(viewModel::deleteLocally)
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.onDeleteForEveryoneTapped = {
- for (message in messages) {
- viewModel.deleteForEveryone(message)
- }
+ messages.forEach(viewModel::deleteForEveryone)
bottomSheet.dismiss()
endActionMode()
}
@@ -1646,56 +1918,37 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode()
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
- } else {
+ }
+ else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
+ {
val messageCount = 1
- val builder = AlertDialog.Builder(this)
- builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
- builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
- builder.setCancelable(true)
- builder.setPositiveButton(R.string.delete) { _, _ ->
- for (message in messages) {
- viewModel.deleteLocally(message)
+ showSessionDialog {
+ title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
+ text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
+ button(R.string.delete) {
+ messages.forEach(viewModel::deleteLocally); endActionMode()
}
- endActionMode()
+ cancelButton(::endActionMode)
}
- builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
- endActionMode()
- }
- builder.show()
}
}
override fun banUser(messages: Set) {
- val builder = AlertDialog.Builder(this)
- builder.setTitle(R.string.ConversationFragment_ban_selected_user)
- builder.setMessage("This will ban the selected user from this room. It won't ban them from other rooms.")
- builder.setCancelable(true)
- builder.setPositiveButton(R.string.ban) { _, _ ->
- viewModel.banUser(messages.first().individualRecipient)
- endActionMode()
+ showSessionDialog {
+ title(R.string.ConversationFragment_ban_selected_user)
+ text("This will ban the selected user from this room. It won't ban them from other rooms.")
+ button(R.string.ban) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
+ cancelButton(::endActionMode)
}
- builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
- endActionMode()
- }
- builder.show()
}
override fun banAndDeleteAll(messages: Set) {
- val builder = AlertDialog.Builder(this)
- builder.setTitle(R.string.ConversationFragment_ban_selected_user)
- builder.setMessage("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
- builder.setCancelable(true)
- builder.setPositiveButton(R.string.ban) { _, _ ->
- viewModel.banAndDeleteAll(messages.first().individualRecipient)
- endActionMode()
+ showSessionDialog {
+ title(R.string.ConversationFragment_ban_selected_user)
+ text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
+ button(R.string.ban) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
+ cancelButton(::endActionMode)
}
- builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
- endActionMode()
- }
- builder.show()
}
override fun copyMessages(messages: Set) {
@@ -1736,6 +1989,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode()
}
+ override fun resyncMessage(messages: Set) {
+ messages.iterator().forEach { messageRecord ->
+ ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey, isResync = true)
+ }
+ endActionMode()
+ }
+
override fun resendMessage(messages: Set) {
messages.iterator().forEach { messageRecord ->
ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey)
@@ -1743,16 +2003,37 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode()
}
+ private val handleMessageDetail = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ val message = result.data?.extras?.getLong(MESSAGE_TIMESTAMP)
+ ?.let(mmsSmsDb::getMessageForTimestamp)
+
+ val set = setOfNotNull(message)
+
+ when (result.resultCode) {
+ ON_REPLY -> reply(set)
+ ON_RESEND -> resendMessage(set)
+ ON_DELETE -> deleteMessages(set)
+ }
+ }
+
override fun showMessageDetail(messages: Set) {
- val intent = Intent(this, MessageDetailActivity::class.java)
- intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp)
- push(intent)
+ Intent(this, MessageDetailActivity::class.java)
+ .apply { putExtra(MESSAGE_TIMESTAMP, messages.first().timestamp) }
+ .let { handleMessageDetail.launch(it) }
+
endActionMode()
}
override fun saveAttachment(messages: Set) {
val message = messages.first() as MmsMessageRecord
- SaveAttachmentTask.showWarningDialog(this, { _, _ ->
+
+ // Do not allow the user to download a file attachment before it has finished downloading
+ if (message.isMediaPending) {
+ Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show()
+ return
+ }
+
+ SaveAttachmentTask.showWarningDialog(this) {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
@@ -1780,15 +2061,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
Toast.LENGTH_LONG).show()
}
.execute()
- })
+ }
}
override fun reply(messages: Set) {
val recipient = viewModel.recipient ?: return
- binding?.inputBar?.draftQuote(recipient, messages.first(), glide)
+ messages.firstOrNull()?.let { binding?.inputBar?.draftQuote(recipient, it, glide) }
endActionMode()
}
+ override fun destroyActionMode() {
+ this.actionMode = null
+ }
+
private fun sendScreenshotNotification() {
val recipient = viewModel.recipient ?: return
if (recipient.isGroupRecipient) return
@@ -1800,7 +2085,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun sendMediaSavedNotification() {
val recipient = viewModel.recipient ?: return
if (recipient.isGroupRecipient) { return }
- val timestamp = System.currentTimeMillis()
+ val timestamp = SnodeAPI.nowWithOffset
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
val message = DataExtractionNotification(kind)
MessageSender.send(message, recipient.address)
@@ -1834,7 +2119,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (result == null) return@Observer
if (result.getResults().isNotEmpty()) {
result.getResults()[result.position]?.let {
- jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs) {
+ jumpToMessage(it.messageRecipient.address, it.sentTimestampMs, true) {
searchViewModel.onMissingResult() }
}
}
@@ -1871,15 +2156,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
this.searchViewModel.onMoveDown()
}
- private fun jumpToMessage(author: Address, timestamp: Long, onMessageNotFound: Runnable?) {
+ private fun jumpToMessage(author: Address, timestamp: Long, highlight: Boolean, onMessageNotFound: Runnable?) {
SimpleTask.run(lifecycle, {
- mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, timestamp, author)
- }) { p: Int -> moveToMessagePosition(p, onMessageNotFound) }
+ mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, timestamp, author, reverseMessageList)
+ }) { p: Int -> moveToMessagePosition(p, highlight, onMessageNotFound) }
}
- private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) {
+ private fun moveToMessagePosition(position: Int, highlight: Boolean, onMessageNotFound: Runnable?) {
if (position >= 0) {
binding?.conversationRecyclerView?.scrollToPosition(position)
+
+ if (highlight) {
+ runOnUiThread {
+ highlightViewAtPosition(position)
+ }
+ }
} else {
onMessageNotFound?.run()
}
@@ -1892,6 +2183,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val selectedItems = setOf(message)
when (action) {
ConversationReactionOverlay.Action.REPLY -> reply(selectedItems)
+ ConversationReactionOverlay.Action.RESYNC -> resyncMessage(selectedItems)
ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems)
ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachment(selectedItems)
ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems)
@@ -1900,7 +2192,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems)
ConversationReactionOverlay.Action.BAN_AND_DELETE_ALL -> banAndDeleteAll(selectedItems)
ConversationReactionOverlay.Action.BAN_USER -> banUser(selectedItems)
- ConversationReactionOverlay.Action.COPY_SESSION_ID -> TODO()
+ ConversationReactionOverlay.Action.COPY_SESSION_ID -> copySessionID(selectedItems)
+ }
+ }
+ }
+
+ // AdapterDataObserver implementation to scroll us to the bottom of the ConversationRecyclerView
+ // when we're already near the bottom and we send or receive a message.
+ inner class ConversationAdapterDataObserver(val recyclerView: ConversationRecyclerView, val adapter: ConversationAdapter) : RecyclerView.AdapterDataObserver() {
+ override fun onChanged() {
+ super.onChanged()
+ if (recyclerView.isScrolledToWithin30dpOfBottom) {
+ // Note: The adapter itemCount is zero based - so calling this with the itemCount in
+ // a non-zero based manner scrolls us to the bottom of the last message (including
+ // to the bottom of long messages as required by Jira SES-789 / GitHub 1364).
+ recyclerView.scrollToPosition(adapter.itemCount)
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
index 17a47a843f..d051d7d93c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
@@ -1,14 +1,11 @@
package org.thoughtcrime.securesms.conversation.v2
-import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.util.SparseArray
import android.util.SparseBooleanArray
-import android.view.LayoutInflater
import android.view.MotionEvent
-import android.view.View
import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.core.util.getOrDefault
@@ -21,7 +18,6 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import network.loki.messenger.R
-import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
@@ -31,18 +27,23 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
+import org.thoughtcrime.securesms.showSessionDialog
+import java.util.concurrent.atomic.AtomicLong
+import kotlin.math.min
class ConversationAdapter(
context: Context,
cursor: Cursor,
+ originalLastSeen: Long,
+ private val isReversed: Boolean,
private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit,
private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit,
private val onDeselect: (MessageRecord, Int) -> Unit,
+ private val onAttachmentNeedsDownload: (Long, Long) -> Unit,
private val glide: GlideRequests,
lifecycleCoroutineScope: LifecycleCoroutineScope
-)
- : CursorRecyclerViewAdapter(context, cursor) {
+) : CursorRecyclerViewAdapter(context, cursor) {
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
var selectedItems = mutableSetOf()
@@ -52,6 +53,9 @@ class ConversationAdapter(
private val updateQueue = Channel(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val contactCache = SparseArray(100)
private val contactLoadedCache = SparseBooleanArray(100)
+ private val lastSeen = AtomicLong(originalLastSeen)
+ private var lastSentMessageId: Long = -1L
+
init {
lifecycleCoroutineScope.launch(IO) {
while (isActive) {
@@ -81,7 +85,7 @@ class ConversationAdapter(
}
}
- class VisibleMessageViewHolder(val view: View) : ViewHolder(view)
+ class VisibleMessageViewHolder(val view: VisibleMessageView) : ViewHolder(view)
class ControlMessageViewHolder(val view: ControlMessageView) : ViewHolder(view)
override fun getItemViewType(cursor: Cursor): Int {
@@ -94,7 +98,7 @@ class ConversationAdapter(
@Suppress("NAME_SHADOWING")
val viewType = ViewType.allValues[viewType]
return when (viewType) {
- ViewType.Visible -> VisibleMessageViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_visible_message, parent, false))
+ ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context))
ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
else -> throw IllegalStateException("Unexpected view type: $viewType.")
}
@@ -106,7 +110,7 @@ class ConversationAdapter(
val messageBefore = getMessageBefore(position, cursor)
when (viewHolder) {
is VisibleMessageViewHolder -> {
- val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
+ val visibleMessageView = viewHolder.view
val isSelected = selectedItems.contains(message)
visibleMessageView.snIsSelected = isSelected
visibleMessageView.indexInAdapter = position
@@ -120,7 +124,20 @@ class ConversationAdapter(
}
val contact = contactCache[senderIdHash]
- visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId, visibleMessageViewDelegate)
+ visibleMessageView.bind(
+ message,
+ messageBefore,
+ getMessageAfter(position, cursor),
+ glide,
+ searchQuery,
+ contact,
+ senderId,
+ lastSeen.get(),
+ visibleMessageViewDelegate,
+ onAttachmentNeedsDownload,
+ lastSentMessageId
+ )
+
if (!message.isDeleted) {
visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
@@ -135,17 +152,15 @@ class ConversationAdapter(
viewHolder.view.bind(message, messageBefore)
if (message.isCallLog && message.isFirstMissedCall) {
viewHolder.view.setOnClickListener {
- AlertDialog.Builder(context)
- .setTitle(R.string.CallNotificationBuilder_first_call_title)
- .setMessage(R.string.CallNotificationBuilder_first_call_message)
- .setPositiveButton(R.string.activity_settings_title) { _, _ ->
- val intent = Intent(context, PrivacySettingsActivity::class.java)
- context.startActivity(intent)
+ context.showSessionDialog {
+ title(R.string.CallNotificationBuilder_first_call_title)
+ text(R.string.CallNotificationBuilder_first_call_message)
+ button(R.string.activity_settings_title) {
+ Intent(context, PrivacySettingsActivity::class.java)
+ .let(context::startActivity)
}
- .setNeutralButton(R.string.cancel) { d, _ ->
- d.dismiss()
- }
- .show()
+ cancelButton()
+ }
}
} else {
viewHolder.view.setOnClickListener(null)
@@ -161,7 +176,7 @@ class ConversationAdapter(
override fun onItemViewRecycled(viewHolder: ViewHolder?) {
when (viewHolder) {
- is VisibleMessageViewHolder -> viewHolder.view.findViewById(R.id.visibleMessageView).recycle()
+ is VisibleMessageViewHolder -> viewHolder.view.recycle()
is ControlMessageViewHolder -> viewHolder.view.recycle()
}
super.onItemViewRecycled(viewHolder)
@@ -174,19 +189,24 @@ class ConversationAdapter(
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
// The message that's visually before the current one is actually after the current
// one for the cursor because the layout is reversed
- if (!cursor.moveToPosition(position + 1)) { return null }
+ if (isReversed && !cursor.moveToPosition(position + 1)) { return null }
+ if (!isReversed && !cursor.moveToPosition(position - 1)) { return null }
+
return messageDB.readerFor(cursor).current
}
private fun getMessageAfter(position: Int, cursor: Cursor): MessageRecord? {
// The message that's visually after the current one is actually before the current
// one for the cursor because the layout is reversed
- if (!cursor.moveToPosition(position - 1)) { return null }
+ if (isReversed && !cursor.moveToPosition(position - 1)) { return null }
+ if (!isReversed && !cursor.moveToPosition(position + 1)) { return null }
+
return messageDB.readerFor(cursor).current
}
override fun changeCursor(cursor: Cursor?) {
super.changeCursor(cursor)
+
val toRemove = mutableSetOf()
val toDeselect = mutableSetOf>()
for (selected in selectedItems) {
@@ -208,11 +228,30 @@ class ConversationAdapter(
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
val cursor = this.cursor
- if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null
+ if (cursor == null || !isActiveCursor) return null
+ if (lastSeenTimestamp == 0L) {
+ if (isReversed && cursor.moveToLast()) { return cursor.position }
+ if (!isReversed && cursor.moveToFirst()) { return cursor.position }
+ }
+
+ // Loop from the newest message to the oldest until we find one older (or equal to)
+ // the lastSeenTimestamp, then return that message index
for (i in 0 until itemCount) {
- cursor.moveToPosition(i)
- val message = messageDB.readerFor(cursor).current
- if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i }
+ if (isReversed) {
+ cursor.moveToPosition(i)
+ val (outgoing, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
+ if (outgoing || dateSent <= lastSeenTimestamp) {
+ return i
+ }
+ }
+ else {
+ val index = ((itemCount - 1) - i)
+ cursor.moveToPosition(index)
+ val (outgoing, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
+ if (outgoing || dateSent <= lastSeenTimestamp) {
+ return min(itemCount - 1, (index + 1))
+ }
+ }
}
return null
}
@@ -222,8 +261,8 @@ class ConversationAdapter(
if (timestamp <= 0L || cursor == null || !isActiveCursor) return null
for (i in 0 until itemCount) {
cursor.moveToPosition(i)
- val message = messageDB.readerFor(cursor).current
- if (message.dateSent == timestamp) { return i }
+ val (_, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
+ if (dateSent == timestamp) { return i }
}
return null
}
@@ -232,4 +271,11 @@ class ConversationAdapter(
this.searchQuery = query
notifyDataSetChanged()
}
+
+ fun getTimestampForItemAt(firstVisiblePosition: Int): Long? {
+ val cursor = this.cursor ?: return null
+ if (!cursor.moveToPosition(firstVisiblePosition)) return null
+ val message = messageDB.readerFor(cursor).current ?: return null
+ return message.timestamp
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt
index 4692bf7862..2ac613bf66 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import android.database.Cursor
+import org.session.libsession.messaging.MessagingModuleConfiguration
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.AbstractCursorLoader
@@ -12,6 +13,7 @@ class ConversationLoader(
) : AbstractCursorLoader(context) {
override fun getCursor(): Cursor {
+ MessagingModuleConfiguration.shared.lastSentTimestampCache.refresh(threadID)
return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java
deleted file mode 100644
index 995dcda2f2..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java
+++ /dev/null
@@ -1,888 +0,0 @@
-package org.thoughtcrime.securesms.conversation.v2;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.util.AttributeSet;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.Window;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.core.content.ContextCompat;
-import androidx.core.view.ViewKt;
-import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
-
-import com.annimon.stream.Stream;
-
-import org.session.libsession.messaging.open_groups.OpenGroup;
-import org.session.libsession.utilities.TextSecurePreferences;
-import org.session.libsession.utilities.ThemeUtil;
-import org.session.libsession.utilities.recipients.Recipient;
-import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
-import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
-import org.thoughtcrime.securesms.components.menu.ActionItem;
-import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper;
-import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
-import org.thoughtcrime.securesms.database.model.MessageRecord;
-import org.thoughtcrime.securesms.database.model.ReactionRecord;
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
-import org.thoughtcrime.securesms.util.AnimationCompleteListener;
-import org.thoughtcrime.securesms.util.DateUtils;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-
-import kotlin.Unit;
-import network.loki.messenger.R;
-
-public final class ConversationReactionOverlay extends FrameLayout {
-
- public static final float LONG_PRESS_SCALE_FACTOR = 0.95f;
- private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
-
- private final Rect emojiViewGlobalRect = new Rect();
- private final Rect emojiStripViewBounds = new Rect();
- private float segmentSize;
-
- private final Boundary horizontalEmojiBoundary = new Boundary();
- private final Boundary verticalScrubBoundary = new Boundary();
- private final PointF deadzoneTouchPoint = new PointF();
-
- private Activity activity;
- private MessageRecord messageRecord;
- private SelectedConversationModel selectedConversationModel;
- private String blindedPublicKey;
- private OverlayState overlayState = OverlayState.HIDDEN;
- private RecentEmojiPageModel recentEmojiPageModel;
-
- private boolean downIsOurs;
- private int selected = -1;
- private int customEmojiIndex;
- private int originalStatusBarColor;
- private int originalNavigationBarColor;
-
- private View dropdownAnchor;
- private LinearLayout conversationItem;
- private View backgroundView;
- private ConstraintLayout foregroundView;
- private EmojiImageView[] emojiViews;
-
- private ConversationContextMenu contextMenu;
-
- private float touchDownDeadZoneSize;
- private float distanceFromTouchDownPointToBottomOfScrubberDeadZone;
- private int scrubberWidth;
- private int selectedVerticalTranslation;
- private int scrubberHorizontalMargin;
- private int animationEmojiStartDelayFactor;
- private int statusBarHeight;
-
- private OnReactionSelectedListener onReactionSelectedListener;
- private OnActionSelectedListener onActionSelectedListener;
- private OnHideListener onHideListener;
-
- private AnimatorSet revealAnimatorSet = new AnimatorSet();
- private AnimatorSet hideAnimatorSet = new AnimatorSet();
-
- public ConversationReactionOverlay(@NonNull Context context) {
- super(context);
- }
-
- public ConversationReactionOverlay(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- dropdownAnchor = findViewById(R.id.dropdown_anchor);
- conversationItem = findViewById(R.id.conversation_item);
- backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
- foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
-
- emojiViews = new EmojiImageView[] { findViewById(R.id.reaction_1),
- findViewById(R.id.reaction_2),
- findViewById(R.id.reaction_3),
- findViewById(R.id.reaction_4),
- findViewById(R.id.reaction_5),
- findViewById(R.id.reaction_6),
- findViewById(R.id.reaction_7) };
-
- customEmojiIndex = emojiViews.length - 1;
-
- distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom);
-
- touchDownDeadZoneSize = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size);
- scrubberWidth = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width);
- selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation);
- scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin);
-
- animationEmojiStartDelayFactor = getResources().getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor);
-
- initAnimators();
- }
-
- public void show(@NonNull Activity activity,
- @NonNull MessageRecord messageRecord,
- @NonNull PointF lastSeenDownPoint,
- @NonNull SelectedConversationModel selectedConversationModel,
- @Nullable String blindedPublicKey)
- {
- if (overlayState != OverlayState.HIDDEN) {
- return;
- }
-
- this.messageRecord = messageRecord;
- this.selectedConversationModel = selectedConversationModel;
- this.blindedPublicKey = blindedPublicKey;
- overlayState = OverlayState.UNINITAILIZED;
- selected = -1;
- recentEmojiPageModel = new RecentEmojiPageModel(activity);
-
- setupSelectedEmoji();
-
- View statusBarBackground = activity.findViewById(android.R.id.statusBarBackground);
- statusBarHeight = statusBarBackground == null ? 0 : statusBarBackground.getHeight();
-
- Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
-
- View conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble);
- conversationBubble.setLayoutParams(new LinearLayout.LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
- conversationBubble.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));
- TextView conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
- conversationTimestamp.setText(DateUtils.getDisplayFormattedTimeSpanString(getContext(), Locale.getDefault(), messageRecord.getTimestamp()));
-
- updateConversationTimestamp(messageRecord);
-
- boolean isMessageOnLeft = selectedConversationModel.isOutgoing() ^ ViewUtil.isLtr(this);
-
- conversationItem.setScaleX(LONG_PRESS_SCALE_FACTOR);
- conversationItem.setScaleY(LONG_PRESS_SCALE_FACTOR);
-
- setVisibility(View.INVISIBLE);
-
- this.activity = activity;
- updateSystemUiOnShow(activity);
-
- ViewKt.doOnLayout(this, v -> {
- showAfterLayout(messageRecord, lastSeenDownPoint, isMessageOnLeft);
- return Unit.INSTANCE;
- });
- }
-
- private void updateConversationTimestamp(MessageRecord message) {
- View bubble = conversationItem.findViewById(R.id.conversation_item_bubble);
- View timestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
- conversationItem.removeAllViewsInLayout();
- conversationItem.addView(message.isOutgoing() ? timestamp : bubble);
- conversationItem.addView(message.isOutgoing() ? bubble : timestamp);
- conversationItem.requestLayout();
- }
-
- private void showAfterLayout(@NonNull MessageRecord messageRecord,
- @NonNull PointF lastSeenDownPoint,
- boolean isMessageOnLeft) {
- contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(messageRecord));
-
- float itemX = isMessageOnLeft ? scrubberHorizontalMargin :
- selectedConversationModel.getBubbleX() - conversationItem.getWidth() + selectedConversationModel.getBubbleWidth();
- conversationItem.setX(itemX);
- conversationItem.setY(selectedConversationModel.getBubbleY() - statusBarHeight);
-
- Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
- boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth();
-
- int overlayHeight = getHeight();
- int bubbleWidth = selectedConversationModel.getBubbleWidth();
-
- float endX = itemX;
- float endY = conversationItem.getY();
- float endApparentTop = endY;
- float endScale = 1f;
-
- float menuPadding = DimensionUnit.DP.toPixels(12f);
- float reactionBarTopPadding = DimensionUnit.DP.toPixels(32f);
- int reactionBarHeight = backgroundView.getHeight();
-
- float reactionBarBackgroundY;
-
- if (isWideLayout) {
- boolean everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.getHeight() < overlayHeight;
- if (everythingFitsVertically) {
- boolean reactionBarFitsAboveItem = conversationItem.getY() > reactionBarHeight + menuPadding + reactionBarTopPadding;
-
- if (reactionBarFitsAboveItem) {
- reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
- } else {
- endY = reactionBarHeight + menuPadding + reactionBarTopPadding;
- reactionBarBackgroundY = reactionBarTopPadding;
- }
- } else {
- float spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding;
-
- endScale = spaceAvailableForItem / conversationItem.getHeight();
- endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
- endY = reactionBarHeight + menuPadding + reactionBarTopPadding - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
- reactionBarBackgroundY = reactionBarTopPadding;
- }
- } else {
- float reactionBarOffset = DimensionUnit.DP.toPixels(48);
- float spaceForReactionBar = Math.max(reactionBarHeight + reactionBarOffset, 0);
- boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + spaceForReactionBar < overlayHeight;
-
- if (everythingFitsVertically) {
- float bubbleBottom = selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
- boolean menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight;
-
- if (menuFitsBelowItem) {
- if (conversationItem.getY() < 0) {
- endY = 0;
- }
- float contextMenuTop = endY + conversationItemSnapshot.getHeight();
- reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
-
- if (reactionBarBackgroundY <= reactionBarTopPadding) {
- endY = backgroundView.getHeight() + menuPadding + reactionBarTopPadding;
- }
- } else {
- endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight();
-
- float contextMenuTop = endY + conversationItemSnapshot.getHeight();
- reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
- }
-
- endApparentTop = endY;
- } else if (reactionBarOffset + reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < overlayHeight) {
- float spaceAvailableForItem = (float) overlayHeight - contextMenu.getMaxHeight() - menuPadding - spaceForReactionBar;
-
- endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
- endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
- endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
-
- float contextMenuTop = endY + (conversationItemSnapshot.getHeight() * endScale);
- reactionBarBackgroundY = reactionBarTopPadding;//getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
- endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
- } else {
- contextMenu.setHeight(contextMenu.getMaxHeight() / 2);
-
- int menuHeight = contextMenu.getHeight();
- boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight;
-
- if (fitsVertically) {
- float bubbleBottom = selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
- boolean menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight;
-
- if (menuFitsBelowItem) {
- reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
-
- if (reactionBarBackgroundY < reactionBarTopPadding) {
- endY = reactionBarTopPadding + reactionBarHeight + menuPadding;
- reactionBarBackgroundY = reactionBarTopPadding;
- }
- } else {
- endY = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.getHeight();
- reactionBarBackgroundY = endY - reactionBarHeight - menuPadding;
- }
- endApparentTop = endY;
- } else {
- float spaceAvailableForItem = (float) overlayHeight - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding;
-
- endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
- endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
- endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale) + menuPadding + reactionBarTopPadding;
- reactionBarBackgroundY = reactionBarTopPadding;
- endApparentTop = reactionBarHeight + menuPadding + reactionBarTopPadding;
- }
- }
- }
-
- reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight);
-
- hideAnimatorSet.end();
- setVisibility(View.VISIBLE);
-
- float scrubberX;
- if (isMessageOnLeft) {
- scrubberX = scrubberHorizontalMargin;
- } else {
- scrubberX = getWidth() - scrubberWidth - scrubberHorizontalMargin;
- }
-
- foregroundView.setX(scrubberX);
- foregroundView.setY(reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.getHeight() / 2f);
-
- backgroundView.setX(scrubberX);
- backgroundView.setY(reactionBarBackgroundY);
-
- verticalScrubBoundary.update(reactionBarBackgroundY,
- lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
-
- updateBoundsOnLayoutChanged();
-
- revealAnimatorSet.start();
-
- if (isWideLayout) {
- float scrubberRight = scrubberX + scrubberWidth;
- float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding;
- contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), overlayHeight - contextMenu.getMaxHeight()));
- } else {
- float contentX = isMessageOnLeft ? scrubberHorizontalMargin : selectedConversationModel.getBubbleX();
- float offsetX = isMessageOnLeft ? contentX : -contextMenu.getMaxWidth() + contentX + bubbleWidth;
-
- float menuTop = endApparentTop + (conversationItemSnapshot.getHeight() * endScale);
- contextMenu.show((int) offsetX, (int) (menuTop + menuPadding));
- }
-
- int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
-
- conversationItem.animate()
- .x(endX)
- .y(endY)
- .scaleX(endScale)
- .scaleY(endScale)
- .setDuration(revealDuration);
- }
-
- private float getReactionBarOffsetForTouch(float itemY,
- float contextMenuTop,
- float contextMenuPadding,
- float reactionBarOffset,
- int reactionBarHeight,
- float spaceNeededBetweenTopOfScreenAndTopOfReactionBar,
- float messageTop)
- {
- float adjustedTouchY = itemY - statusBarHeight;
- float reactionStartingPoint = Math.min(adjustedTouchY, contextMenuTop);
-
- float spaceBetweenTopOfMessageAndTopOfContextMenu = Math.abs(messageTop - contextMenuTop);
-
- if (spaceBetweenTopOfMessageAndTopOfContextMenu < DimensionUnit.DP.toPixels(150)) {
- float offsetToMakeReactionBarOffsetMatchMenuPadding = reactionBarOffset - contextMenuPadding;
- reactionStartingPoint = messageTop + offsetToMakeReactionBarOffsetMatchMenuPadding;
- }
-
- return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar);
- }
-
- private void updateSystemUiOnShow(@NonNull Activity activity) {
- Window window = activity.getWindow();
- int barColor = ContextCompat.getColor(getContext(), R.color.reactions_screen_dark_shade_color);
-
- originalStatusBarColor = window.getStatusBarColor();
- WindowUtil.setStatusBarColor(window, barColor);
-
- originalNavigationBarColor = window.getNavigationBarColor();
- WindowUtil.setNavigationBarColor(window, barColor);
-
- if (!ThemeUtil.isDarkTheme(getContext())) {
- WindowUtil.clearLightStatusBar(window);
- WindowUtil.clearLightNavigationBar(window);
- }
- }
-
- public void hide() {
- hideInternal(onHideListener);
- }
-
- public void hideForReactWithAny() {
- hideInternal(onHideListener);
- }
-
- private void hideInternal(@Nullable OnHideListener onHideListener) {
- overlayState = OverlayState.HIDDEN;
-
- AnimatorSet animatorSet = newHideAnimatorSet();
- hideAnimatorSet = animatorSet;
-
- revealAnimatorSet.end();
- animatorSet.start();
-
- if (onHideListener != null) {
- onHideListener.startHide();
- }
-
- if (selectedConversationModel.getFocusedView() != null) {
- ViewUtil.focusAndShowKeyboard(selectedConversationModel.getFocusedView());
- }
-
- animatorSet.addListener(new AnimationCompleteListener() {
- @Override public void onAnimationEnd(Animator animation) {
- animatorSet.removeListener(this);
-
- if (onHideListener != null) {
- onHideListener.onHide();
- }
- }
- });
-
- if (contextMenu != null) {
- contextMenu.dismiss();
- }
- }
-
- public boolean isShowing() {
- return overlayState != OverlayState.HIDDEN;
- }
-
- public @NonNull MessageRecord getMessageRecord() {
- return messageRecord;
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
-
- updateBoundsOnLayoutChanged();
- }
-
- private void updateBoundsOnLayoutChanged() {
- backgroundView.getGlobalVisibleRect(emojiStripViewBounds);
- emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect);
- emojiStripViewBounds.left = getStart(emojiViewGlobalRect);
- emojiViews[emojiViews.length - 1].getGlobalVisibleRect(emojiViewGlobalRect);
- emojiStripViewBounds.right = getEnd(emojiViewGlobalRect);
-
- segmentSize = emojiStripViewBounds.width() / (float) emojiViews.length;
- }
-
- private int getStart(@NonNull Rect rect) {
- if (ViewUtil.isLtr(this)) {
- return rect.left;
- } else {
- return rect.right;
- }
- }
-
- private int getEnd(@NonNull Rect rect) {
- if (ViewUtil.isLtr(this)) {
- return rect.right;
- } else {
- return rect.left;
- }
- }
-
- public boolean applyTouchEvent(@NonNull MotionEvent motionEvent) {
- if (!isShowing()) {
- throw new IllegalStateException("Touch events should only be propagated to this method if we are displaying the scrubber.");
- }
-
- if ((motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) != 0) {
- return true;
- }
-
- if (overlayState == OverlayState.UNINITAILIZED) {
- downIsOurs = false;
-
- deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());
-
- overlayState = OverlayState.DEADZONE;
- }
-
- if (overlayState == OverlayState.DEADZONE) {
- float deltaX = Math.abs(deadzoneTouchPoint.x - motionEvent.getX());
- float deltaY = Math.abs(deadzoneTouchPoint.y - motionEvent.getY());
-
- if (deltaX > touchDownDeadZoneSize || deltaY > touchDownDeadZoneSize) {
- overlayState = OverlayState.SCRUB;
- } else {
- if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
- overlayState = OverlayState.TAP;
-
- if (downIsOurs) {
- handleUpEvent();
- return true;
- }
- }
-
- return MotionEvent.ACTION_MOVE == motionEvent.getAction();
- }
- }
-
- switch (motionEvent.getAction()) {
- case MotionEvent.ACTION_DOWN:
- selected = getSelectedIndexViaDownEvent(motionEvent);
-
- deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());
- overlayState = OverlayState.DEADZONE;
- downIsOurs = true;
- return true;
- case MotionEvent.ACTION_MOVE:
- selected = getSelectedIndexViaMoveEvent(motionEvent);
- return true;
- case MotionEvent.ACTION_UP:
- handleUpEvent();
- return downIsOurs;
- case MotionEvent.ACTION_CANCEL:
- hide();
- return downIsOurs;
- default:
- return false;
- }
- }
-
- private void setupSelectedEmoji() {
- final List emojis = recentEmojiPageModel.getEmoji();
-
- for (int i = 0; i < emojiViews.length; i++) {
- final EmojiImageView view = emojiViews[i];
-
- view.setScaleX(1.0f);
- view.setScaleY(1.0f);
- view.setTranslationY(0);
-
- boolean isAtCustomIndex = i == customEmojiIndex;
-
- if (isAtCustomIndex) {
- view.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_baseline_add_24));
- view.setTag(null);
- } else {
- view.setImageEmoji(emojis.get(i));
- }
- }
- }
-
- private int getSelectedIndexViaDownEvent(@NonNull MotionEvent motionEvent) {
- return getSelectedIndexViaMotionEvent(motionEvent, new Boundary(emojiStripViewBounds.top, emojiStripViewBounds.bottom));
- }
-
- private int getSelectedIndexViaMoveEvent(@NonNull MotionEvent motionEvent) {
- return getSelectedIndexViaMotionEvent(motionEvent, verticalScrubBoundary);
- }
-
- private int getSelectedIndexViaMotionEvent(@NonNull MotionEvent motionEvent, @NonNull Boundary boundary) {
- int selected = -1;
-
- if (backgroundView.getVisibility() != View.VISIBLE) {
- return selected;
- }
-
- for (int i = 0; i < emojiViews.length; i++) {
- final float emojiLeft = (segmentSize * i) + emojiStripViewBounds.left;
- horizontalEmojiBoundary.update(emojiLeft, emojiLeft + segmentSize);
-
- if (horizontalEmojiBoundary.contains(motionEvent.getX()) && boundary.contains(motionEvent.getY())) {
- selected = i;
- }
- }
-
- if (this.selected != -1 && this.selected != selected) {
- shrinkView(emojiViews[this.selected]);
- }
-
- if (this.selected != selected && selected != -1) {
- growView(emojiViews[selected]);
- }
-
- return selected;
- }
-
- private void growView(@NonNull View view) {
- view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
- view.animate()
- .scaleY(1.5f)
- .scaleX(1.5f)
- .translationY(-selectedVerticalTranslation)
- .setDuration(200)
- .setInterpolator(INTERPOLATOR)
- .start();
- }
-
- private void shrinkView(@NonNull View view) {
- view.animate()
- .scaleX(1.0f)
- .scaleY(1.0f)
- .translationY(0)
- .setDuration(200)
- .setInterpolator(INTERPOLATOR)
- .start();
- }
-
- private void handleUpEvent() {
- if (selected != -1 && onReactionSelectedListener != null && backgroundView.getVisibility() == View.VISIBLE) {
- if (selected == customEmojiIndex) {
- onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
- } else {
- onReactionSelectedListener.onReactionSelected(messageRecord, recentEmojiPageModel.getEmoji().get(selected));
- }
- } else {
- hide();
- }
- }
-
- public void setOnReactionSelectedListener(@Nullable OnReactionSelectedListener onReactionSelectedListener) {
- this.onReactionSelectedListener = onReactionSelectedListener;
- }
-
- public void setOnActionSelectedListener(@Nullable OnActionSelectedListener onActionSelectedListener) {
- this.onActionSelectedListener = onActionSelectedListener;
- }
-
- public void setOnHideListener(@Nullable OnHideListener onHideListener) {
- this.onHideListener = onHideListener;
- }
-
- private @Nullable String getOldEmoji(@NonNull MessageRecord messageRecord) {
- return Stream.of(messageRecord.getReactions())
- .filter(record -> record.getAuthor().equals(TextSecurePreferences.getLocalNumber(getContext())))
- .findFirst()
- .map(ReactionRecord::getEmoji)
- .orElse(null);
- }
-
- private @NonNull List getMenuActionItems(@NonNull MessageRecord message) {
- List items = new ArrayList<>();
-
- // Prepare
- boolean containsControlMessage = message.isUpdate();
- boolean hasText = !message.getBody().isEmpty();
- OpenGroup openGroup = DatabaseComponent.get(getContext()).lokiThreadDatabase().getOpenGroupChat(message.getThreadId());
- Recipient recipient = DatabaseComponent.get(getContext()).threadDatabase().getRecipientForThreadId(message.getThreadId());
- if (recipient == null) return Collections.emptyList();
-
- String userPublicKey = TextSecurePreferences.getLocalNumber(getContext());
- // Select message
- items.add(new ActionItem(R.attr.menu_select_icon, getContext().getResources().getString(R.string.conversation_context__menu_select), () -> handleActionItemClicked(Action.SELECT)));
- // Reply
- if (!message.isPending() && !message.isFailed()) {
- items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_reply), () -> handleActionItemClicked(Action.REPLY)));
- }
- // Copy message text
- if (!containsControlMessage && hasText) {
- items.add(new ActionItem(R.attr.menu_copy_icon, getContext().getResources().getString(R.string.copy), () -> handleActionItemClicked(Action.COPY_MESSAGE)));
- }
- // Copy Session ID
- if (recipient.isGroupRecipient() && !recipient.isOpenGroupRecipient() && !message.getRecipient().getAddress().toString().equals(userPublicKey)) {
- items.add(new ActionItem(R.attr.menu_copy_icon, getContext().getResources().getString(R.string.activity_conversation_menu_copy_session_id), () -> handleActionItemClicked(Action.COPY_SESSION_ID)));
- }
- // Delete message
- if (ConversationMenuItemHelper.userCanDeleteSelectedItems(getContext(), message, openGroup, userPublicKey, blindedPublicKey)) {
- items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.delete), () -> handleActionItemClicked(Action.DELETE)));
- }
- // Ban user
- if (ConversationMenuItemHelper.userCanBanSelectedUsers(getContext(), message, openGroup, userPublicKey, blindedPublicKey)) {
- items.add(new ActionItem(R.attr.menu_block_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_user), () -> handleActionItemClicked(Action.BAN_USER)));
- }
- // Ban and delete all
- if (ConversationMenuItemHelper.userCanBanSelectedUsers(getContext(), message, openGroup, userPublicKey, blindedPublicKey)) {
- items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_and_delete_all), () -> handleActionItemClicked(Action.BAN_AND_DELETE_ALL)));
- }
- // Message detail
- if (message.isFailed()) {
- items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
- }
- // Resend
- if (message.isFailed()) {
- items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
- }
- // Save media
- if (message.isMms() && ((MediaMmsMessageRecord)message).containsMediaSlide()) {
- items.add(new ActionItem(R.attr.menu_save_icon, getContext().getResources().getString(R.string.conversation_context_image__save_attachment), () -> handleActionItemClicked(Action.DOWNLOAD)));
- }
-
- backgroundView.setVisibility(View.VISIBLE);
- foregroundView.setVisibility(View.VISIBLE);
-
- return items;
- }
-
- private void handleActionItemClicked(@NonNull Action action) {
- hideInternal(new OnHideListener() {
- @Override public void startHide() {
- if (onHideListener != null) {
- onHideListener.startHide();
- }
- }
-
- @Override public void onHide() {
- if (onHideListener != null) {
- onHideListener.onHide();
- }
-
- if (onActionSelectedListener != null) {
- onActionSelectedListener.onActionSelected(action);
- }
- }
- });
- }
-
- private void initAnimators() {
-
- int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
- int revealOffset = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_offset);
-
- List reveals = Stream.of(emojiViews)
- .mapIndexed((idx, v) -> {
- Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_reveal);
- anim.setTarget(v);
- anim.setStartDelay(idx * animationEmojiStartDelayFactor);
- return anim;
- })
- .toList();
-
- Animator backgroundRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
- backgroundRevealAnim.setTarget(backgroundView);
- backgroundRevealAnim.setDuration(revealDuration);
- backgroundRevealAnim.setStartDelay(revealOffset);
- reveals.add(backgroundRevealAnim);
-
- revealAnimatorSet.setInterpolator(INTERPOLATOR);
- revealAnimatorSet.playTogether(reveals);
- }
-
- private @NonNull AnimatorSet newHideAnimatorSet() {
- AnimatorSet set = new AnimatorSet();
-
- set.addListener(new AnimationCompleteListener() {
- @Override
- public void onAnimationEnd(Animator animation) {
- setVisibility(View.GONE);
- }
- });
- set.setInterpolator(INTERPOLATOR);
-
- set.playTogether(newHideAnimators());
-
- return set;
- }
-
- private @NonNull List newHideAnimators() {
- int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_hide_duration);
-
- List animators = new ArrayList<>(Stream.of(emojiViews)
- .mapIndexed((idx, v) -> {
- Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
- anim.setTarget(v);
- return anim;
- })
- .toList());
-
- Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
- backgroundHideAnim.setTarget(backgroundView);
- backgroundHideAnim.setDuration(duration);
- animators.add(backgroundHideAnim);
-
- ObjectAnimator itemScaleXAnim = new ObjectAnimator();
- itemScaleXAnim.setProperty(View.SCALE_X);
- itemScaleXAnim.setFloatValues(1f);
- itemScaleXAnim.setTarget(conversationItem);
- itemScaleXAnim.setDuration(duration);
- animators.add(itemScaleXAnim);
-
- ObjectAnimator itemScaleYAnim = new ObjectAnimator();
- itemScaleYAnim.setProperty(View.SCALE_Y);
- itemScaleYAnim.setFloatValues(1f);
- itemScaleYAnim.setTarget(conversationItem);
- itemScaleYAnim.setDuration(duration);
- animators.add(itemScaleYAnim);
-
- ObjectAnimator itemXAnim = new ObjectAnimator();
- itemXAnim.setProperty(View.X);
- itemXAnim.setFloatValues(selectedConversationModel.getBubbleX());
- itemXAnim.setTarget(conversationItem);
- itemXAnim.setDuration(duration);
- animators.add(itemXAnim);
-
- ObjectAnimator itemYAnim = new ObjectAnimator();
- itemYAnim.setProperty(View.Y);
- itemYAnim.setFloatValues(selectedConversationModel.getBubbleY() - statusBarHeight);
- itemYAnim.setTarget(conversationItem);
- itemYAnim.setDuration(duration);
- animators.add(itemYAnim);
-
- if (activity != null) {
- ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor);
- statusBarAnim.setDuration(duration);
- statusBarAnim.addUpdateListener(animation -> {
- WindowUtil.setStatusBarColor(activity.getWindow(), (int) animation.getAnimatedValue());
- });
- animators.add(statusBarAnim);
-
- ValueAnimator navigationBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalNavigationBarColor);
- navigationBarAnim.setDuration(duration);
- navigationBarAnim.addUpdateListener(animation -> {
- WindowUtil.setNavigationBarColor(activity.getWindow(), (int) animation.getAnimatedValue());
- });
- animators.add(navigationBarAnim);
- }
-
- return animators;
- }
-
- public interface OnHideListener {
- void startHide();
- void onHide();
- }
-
- public interface OnReactionSelectedListener {
- void onReactionSelected(@NonNull MessageRecord messageRecord, String emoji);
- void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji);
- }
-
- public interface OnActionSelectedListener {
- void onActionSelected(@NonNull Action action);
- }
-
- private static class Boundary {
- private float min;
- private float max;
-
- Boundary() {}
-
- Boundary(float min, float max) {
- update(min, max);
- }
-
- private void update(float min, float max) {
- this.min = min;
- this.max = max;
- }
-
- public boolean contains(float value) {
- if (min < max) {
- return this.min < value && this.max > value;
- } else {
- return this.min > value && this.max < value;
- }
- }
- }
-
- private enum OverlayState {
- HIDDEN,
- UNINITAILIZED,
- DEADZONE,
- SCRUB,
- TAP
- }
-
- public enum Action {
- REPLY,
- RESEND,
- DOWNLOAD,
- COPY_MESSAGE,
- COPY_SESSION_ID,
- VIEW_INFO,
- SELECT,
- DELETE,
- BAN_USER,
- BAN_AND_DELETE_ALL,
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
new file mode 100644
index 0000000000..af754a300a
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
@@ -0,0 +1,719 @@
+package org.thoughtcrime.securesms.conversation.v2
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.app.Activity
+import android.content.Context
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
+import android.util.AttributeSet
+import android.view.HapticFeedbackConstants
+import android.view.MotionEvent
+import android.view.View
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import androidx.core.view.doOnLayout
+import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import network.loki.messenger.R
+import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
+import org.session.libsession.utilities.ThemeUtil
+import org.thoughtcrime.securesms.components.emoji.EmojiImageView
+import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
+import org.thoughtcrime.securesms.components.menu.ActionItem
+import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
+import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
+import org.thoughtcrime.securesms.database.MmsSmsDatabase
+import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
+import org.thoughtcrime.securesms.database.model.MessageRecord
+import org.thoughtcrime.securesms.database.model.ReactionRecord
+import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
+import org.thoughtcrime.securesms.repository.ConversationRepository
+import org.thoughtcrime.securesms.util.AnimationCompleteListener
+import org.thoughtcrime.securesms.util.DateUtils
+import java.util.Locale
+import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+@AndroidEntryPoint
+class ConversationReactionOverlay : FrameLayout {
+ private val emojiViewGlobalRect = Rect()
+ private val emojiStripViewBounds = Rect()
+ private var segmentSize = 0f
+ private val horizontalEmojiBoundary = Boundary()
+ private val verticalScrubBoundary = Boundary()
+ private val deadzoneTouchPoint = PointF()
+ private lateinit var activity: Activity
+ lateinit var messageRecord: MessageRecord
+ private lateinit var selectedConversationModel: SelectedConversationModel
+ private var blindedPublicKey: String? = null
+ private var overlayState = OverlayState.HIDDEN
+ private lateinit var recentEmojiPageModel: RecentEmojiPageModel
+ private var downIsOurs = false
+ private var selected = -1
+ private var customEmojiIndex = 0
+ private var originalStatusBarColor = 0
+ private var originalNavigationBarColor = 0
+ private lateinit var dropdownAnchor: View
+ private lateinit var conversationItem: LinearLayout
+ private lateinit var conversationBubble: View
+ private lateinit var conversationTimestamp: TextView
+ private lateinit var backgroundView: View
+ private lateinit var foregroundView: ConstraintLayout
+ private lateinit var emojiViews: List
+ private var contextMenu: ConversationContextMenu? = null
+ private var touchDownDeadZoneSize = 0f
+ private var distanceFromTouchDownPointToBottomOfScrubberDeadZone = 0f
+ private var scrubberWidth = 0
+ private var selectedVerticalTranslation = 0
+ private var scrubberHorizontalMargin = 0
+ private var animationEmojiStartDelayFactor = 0
+ private var statusBarHeight = 0
+ private var onReactionSelectedListener: OnReactionSelectedListener? = null
+ private var onActionSelectedListener: OnActionSelectedListener? = null
+ private var onHideListener: OnHideListener? = null
+ private val revealAnimatorSet = AnimatorSet()
+ private var hideAnimatorSet = AnimatorSet()
+
+ @Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
+ @Inject lateinit var repository: ConversationRepository
+ private val scope = CoroutineScope(Dispatchers.Default)
+ private var job: Job? = null
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ dropdownAnchor = findViewById(R.id.dropdown_anchor)
+ conversationItem = findViewById(R.id.conversation_item)
+ conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble)
+ conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp)
+ backgroundView = findViewById(R.id.conversation_reaction_scrubber_background)
+ foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground)
+ emojiViews = listOf(R.id.reaction_1, R.id.reaction_2, R.id.reaction_3, R.id.reaction_4, R.id.reaction_5, R.id.reaction_6, R.id.reaction_7).map { findViewById(it) }
+ customEmojiIndex = emojiViews.size - 1
+ distanceFromTouchDownPointToBottomOfScrubberDeadZone = resources.getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom).toFloat()
+ touchDownDeadZoneSize = resources.getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size).toFloat()
+ scrubberWidth = resources.getDimensionPixelOffset(R.dimen.reaction_scrubber_width)
+ selectedVerticalTranslation = resources.getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation)
+ scrubberHorizontalMargin = resources.getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin)
+ animationEmojiStartDelayFactor = resources.getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor)
+ initAnimators()
+ }
+
+ fun show(activity: Activity,
+ messageRecord: MessageRecord,
+ lastSeenDownPoint: PointF,
+ selectedConversationModel: SelectedConversationModel,
+ blindedPublicKey: String?) {
+ job?.cancel()
+ if (overlayState != OverlayState.HIDDEN) return
+ this.messageRecord = messageRecord
+ this.selectedConversationModel = selectedConversationModel
+ this.blindedPublicKey = blindedPublicKey
+ overlayState = OverlayState.UNINITAILIZED
+ selected = -1
+ recentEmojiPageModel = RecentEmojiPageModel(activity)
+ setupSelectedEmoji()
+ val statusBarBackground = activity.findViewById(android.R.id.statusBarBackground)
+ statusBarHeight = statusBarBackground?.height ?: 0
+ val conversationItemSnapshot = selectedConversationModel.bitmap
+ conversationBubble.layoutParams = LinearLayout.LayoutParams(conversationItemSnapshot.width, conversationItemSnapshot.height)
+ conversationBubble.background = BitmapDrawable(resources, conversationItemSnapshot)
+ conversationTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), messageRecord.timestamp)
+ updateConversationTimestamp(messageRecord)
+ val isMessageOnLeft = selectedConversationModel.isOutgoing xor ViewUtil.isLtr(this)
+ conversationItem.scaleX = LONG_PRESS_SCALE_FACTOR
+ conversationItem.scaleY = LONG_PRESS_SCALE_FACTOR
+ visibility = INVISIBLE
+ this.activity = activity
+ updateSystemUiOnShow(activity)
+ doOnLayout { showAfterLayout(messageRecord, lastSeenDownPoint, isMessageOnLeft) }
+
+ job = scope.launch(Dispatchers.IO) {
+ repository.changes(messageRecord.threadId)
+ .filter { mmsSmsDatabase.getMessageForTimestamp(messageRecord.timestamp) == null }
+ .collect { withContext(Dispatchers.Main) { hide() } }
+ }
+ }
+
+ private fun updateConversationTimestamp(message: MessageRecord) {
+ if (message.isOutgoing) conversationBubble.bringToFront() else conversationTimestamp.bringToFront()
+ }
+
+ private fun showAfterLayout(messageRecord: MessageRecord,
+ lastSeenDownPoint: PointF,
+ isMessageOnLeft: Boolean) {
+ val contextMenu = ConversationContextMenu(dropdownAnchor, getMenuActionItems(messageRecord))
+ this.contextMenu = contextMenu
+ var endX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX - conversationItem.width + selectedConversationModel.bubbleWidth
+ var endY = selectedConversationModel.bubbleY - statusBarHeight
+ conversationItem.x = endX
+ conversationItem.y = endY
+ val conversationItemSnapshot = selectedConversationModel.bitmap
+ val isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < width
+ val overlayHeight = height
+ val bubbleWidth = selectedConversationModel.bubbleWidth
+ var endApparentTop = endY
+ var endScale = 1f
+ val menuPadding = DimensionUnit.DP.toPixels(12f)
+ val reactionBarTopPadding = DimensionUnit.DP.toPixels(32f)
+ val reactionBarHeight = backgroundView.height
+ var reactionBarBackgroundY: Float
+ if (isWideLayout) {
+ val everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.height < overlayHeight
+ if (everythingFitsVertically) {
+ val reactionBarFitsAboveItem = conversationItem.y > reactionBarHeight + menuPadding + reactionBarTopPadding
+ if (reactionBarFitsAboveItem) {
+ reactionBarBackgroundY = conversationItem.y - menuPadding - reactionBarHeight
+ } else {
+ endY = reactionBarHeight + menuPadding + reactionBarTopPadding
+ reactionBarBackgroundY = reactionBarTopPadding
+ }
+ } else {
+ val spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding
+ endScale = spaceAvailableForItem / conversationItem.height
+ endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1
+ endY = reactionBarHeight + menuPadding + reactionBarTopPadding - Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
+ reactionBarBackgroundY = reactionBarTopPadding
+ }
+ } else {
+ val reactionBarOffset = DimensionUnit.DP.toPixels(48f)
+ val spaceForReactionBar = Math.max(reactionBarHeight + reactionBarOffset, 0f)
+ val everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.height + menuPadding + spaceForReactionBar < overlayHeight
+ if (everythingFitsVertically) {
+ val bubbleBottom = selectedConversationModel.bubbleY + conversationItemSnapshot.height
+ val menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight
+ if (menuFitsBelowItem) {
+ if (conversationItem.y < 0) {
+ endY = 0f
+ }
+ val contextMenuTop = endY + conversationItemSnapshot.height
+ reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.bubbleY, contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY)
+ if (reactionBarBackgroundY <= reactionBarTopPadding) {
+ endY = backgroundView.height + menuPadding + reactionBarTopPadding
+ }
+ } else {
+ endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.height
+ reactionBarBackgroundY = endY - reactionBarHeight - menuPadding
+ }
+ endApparentTop = endY
+ } else if (reactionBarOffset + reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < overlayHeight) {
+ val spaceAvailableForItem = overlayHeight.toFloat() - contextMenu.getMaxHeight() - menuPadding - spaceForReactionBar
+ endScale = spaceAvailableForItem / conversationItemSnapshot.height
+ endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1
+ endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
+ val contextMenuTop = endY + conversationItemSnapshot.height * endScale
+ reactionBarBackgroundY = reactionBarTopPadding //getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
+ endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
+ } else {
+ contextMenu.height = contextMenu.getMaxHeight() / 2
+ val menuHeight = contextMenu.height
+ val fitsVertically = menuHeight + conversationItem.height + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight
+ if (fitsVertically) {
+ val bubbleBottom = selectedConversationModel.bubbleY + conversationItemSnapshot.height
+ val menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight
+ if (menuFitsBelowItem) {
+ reactionBarBackgroundY = conversationItem.y - menuPadding - reactionBarHeight
+ if (reactionBarBackgroundY < reactionBarTopPadding) {
+ endY = reactionBarTopPadding + reactionBarHeight + menuPadding
+ reactionBarBackgroundY = reactionBarTopPadding
+ }
+ } else {
+ endY = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.height
+ reactionBarBackgroundY = endY - reactionBarHeight - menuPadding
+ }
+ endApparentTop = endY
+ } else {
+ val spaceAvailableForItem = overlayHeight.toFloat() - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding
+ endScale = spaceAvailableForItem / conversationItemSnapshot.height
+ endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1
+ endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale) + menuPadding + reactionBarTopPadding
+ reactionBarBackgroundY = reactionBarTopPadding
+ endApparentTop = reactionBarHeight + menuPadding + reactionBarTopPadding
+ }
+ }
+ }
+ reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight.toFloat())
+ hideAnimatorSet.end()
+ visibility = VISIBLE
+ val scrubberX = if (isMessageOnLeft) {
+ scrubberHorizontalMargin.toFloat()
+ } else {
+ (width - scrubberWidth - scrubberHorizontalMargin).toFloat()
+ }
+ foregroundView.x = scrubberX
+ foregroundView.y = reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.height / 2f
+ backgroundView.x = scrubberX
+ backgroundView.y = reactionBarBackgroundY
+ verticalScrubBoundary.update(reactionBarBackgroundY,
+ lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone)
+ updateBoundsOnLayoutChanged()
+ revealAnimatorSet.start()
+ if (isWideLayout) {
+ val scrubberRight = scrubberX + scrubberWidth
+ val offsetX = if (isMessageOnLeft) scrubberRight + menuPadding else scrubberX - contextMenu.getMaxWidth() - menuPadding
+ contextMenu.show(offsetX.toInt(), Math.min(backgroundView.y, (overlayHeight - contextMenu.getMaxHeight()).toFloat()).toInt())
+ } else {
+ val contentX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX
+ val offsetX = if (isMessageOnLeft) contentX else -contextMenu.getMaxWidth() + contentX + bubbleWidth
+ val menuTop = endApparentTop + conversationItemSnapshot.height * endScale
+ contextMenu.show(offsetX.toInt(), (menuTop + menuPadding).toInt())
+ }
+ val revealDuration = context.resources.getInteger(R.integer.reaction_scrubber_reveal_duration)
+ conversationBubble.animate()
+ .scaleX(endScale)
+ .scaleY(endScale)
+ .setDuration(revealDuration.toLong())
+ conversationItem.animate()
+ .x(endX)
+ .y(endY)
+ .setDuration(revealDuration.toLong())
+ }
+
+ private fun getReactionBarOffsetForTouch(itemY: Float,
+ contextMenuTop: Float,
+ contextMenuPadding: Float,
+ reactionBarOffset: Float,
+ reactionBarHeight: Int,
+ spaceNeededBetweenTopOfScreenAndTopOfReactionBar: Float,
+ messageTop: Float): Float {
+ val adjustedTouchY = itemY - statusBarHeight
+ var reactionStartingPoint = Math.min(adjustedTouchY, contextMenuTop)
+ val spaceBetweenTopOfMessageAndTopOfContextMenu = Math.abs(messageTop - contextMenuTop)
+ if (spaceBetweenTopOfMessageAndTopOfContextMenu < DimensionUnit.DP.toPixels(150f)) {
+ val offsetToMakeReactionBarOffsetMatchMenuPadding = reactionBarOffset - contextMenuPadding
+ reactionStartingPoint = messageTop + offsetToMakeReactionBarOffsetMatchMenuPadding
+ }
+ return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar)
+ }
+
+ private fun updateSystemUiOnShow(activity: Activity) {
+ val window = activity.window
+ val barColor = ContextCompat.getColor(context, R.color.reactions_screen_dark_shade_color)
+ originalStatusBarColor = window.statusBarColor
+ WindowUtil.setStatusBarColor(window, barColor)
+ originalNavigationBarColor = window.navigationBarColor
+ WindowUtil.setNavigationBarColor(window, barColor)
+ if (!ThemeUtil.isDarkTheme(context)) {
+ WindowUtil.clearLightStatusBar(window)
+ WindowUtil.clearLightNavigationBar(window)
+ }
+ }
+
+ fun hide() {
+ hideInternal(onHideListener)
+ }
+
+ fun hideForReactWithAny() {
+ hideInternal(onHideListener)
+ }
+
+ private fun hideInternal(onHideListener: OnHideListener?) {
+ job?.cancel()
+ overlayState = OverlayState.HIDDEN
+ val animatorSet = newHideAnimatorSet()
+ hideAnimatorSet = animatorSet
+ revealAnimatorSet.end()
+ animatorSet.start()
+ onHideListener?.startHide()
+ selectedConversationModel.focusedView?.let(ViewUtil::focusAndShowKeyboard)
+ animatorSet.addListener(object : AnimationCompleteListener() {
+ override fun onAnimationEnd(animation: Animator) {
+ animatorSet.removeListener(this)
+ onHideListener?.onHide()
+ }
+ })
+ contextMenu?.dismiss()
+ }
+
+ val isShowing: Boolean
+ get() = overlayState != OverlayState.HIDDEN
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ super.onLayout(changed, l, t, r, b)
+ updateBoundsOnLayoutChanged()
+ }
+
+ private fun updateBoundsOnLayoutChanged() {
+ backgroundView.getGlobalVisibleRect(emojiStripViewBounds)
+ emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect)
+ emojiStripViewBounds.left = getStart(emojiViewGlobalRect)
+ emojiViews[emojiViews.size - 1].getGlobalVisibleRect(emojiViewGlobalRect)
+ emojiStripViewBounds.right = getEnd(emojiViewGlobalRect)
+ segmentSize = emojiStripViewBounds.width() / emojiViews.size.toFloat()
+ }
+
+ private fun getStart(rect: Rect): Int = if (ViewUtil.isLtr(this)) rect.left else rect.right
+
+ private fun getEnd(rect: Rect): Int = if (ViewUtil.isLtr(this)) rect.right else rect.left
+
+ fun applyTouchEvent(motionEvent: MotionEvent): Boolean {
+ check(isShowing) { "Touch events should only be propagated to this method if we are displaying the scrubber." }
+ if (motionEvent.action and MotionEvent.ACTION_POINTER_INDEX_MASK != 0) {
+ return true
+ }
+ if (overlayState == OverlayState.UNINITAILIZED) {
+ downIsOurs = false
+ deadzoneTouchPoint[motionEvent.x] = motionEvent.y
+ overlayState = OverlayState.DEADZONE
+ }
+ if (overlayState == OverlayState.DEADZONE) {
+ val deltaX = Math.abs(deadzoneTouchPoint.x - motionEvent.x)
+ val deltaY = Math.abs(deadzoneTouchPoint.y - motionEvent.y)
+ if (deltaX > touchDownDeadZoneSize || deltaY > touchDownDeadZoneSize) {
+ overlayState = OverlayState.SCRUB
+ } else {
+ if (motionEvent.action == MotionEvent.ACTION_UP) {
+ overlayState = OverlayState.TAP
+ if (downIsOurs) {
+ handleUpEvent()
+ return true
+ }
+ }
+ return MotionEvent.ACTION_MOVE == motionEvent.action
+ }
+ }
+ return when (motionEvent.action) {
+ MotionEvent.ACTION_DOWN -> {
+ selected = getSelectedIndexViaDownEvent(motionEvent)
+ deadzoneTouchPoint[motionEvent.x] = motionEvent.y
+ overlayState = OverlayState.DEADZONE
+ downIsOurs = true
+ true
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ selected = getSelectedIndexViaMoveEvent(motionEvent)
+ true
+ }
+
+ MotionEvent.ACTION_UP -> {
+ handleUpEvent()
+ downIsOurs
+ }
+
+ MotionEvent.ACTION_CANCEL -> {
+ hide()
+ downIsOurs
+ }
+
+ else -> false
+ }
+ }
+
+ private fun setupSelectedEmoji() {
+ val emojis = recentEmojiPageModel.emoji
+ emojiViews.forEachIndexed { i, view ->
+ view.scaleX = 1.0f
+ view.scaleY = 1.0f
+ view.translationY = 0f
+ val isAtCustomIndex = i == customEmojiIndex
+ if (isAtCustomIndex) {
+ view.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_baseline_add_24))
+ view.tag = null
+ } else {
+ view.setImageEmoji(emojis[i])
+ }
+ }
+ }
+
+ private fun getSelectedIndexViaDownEvent(motionEvent: MotionEvent): Int =
+ getSelectedIndexViaMotionEvent(motionEvent, Boundary(emojiStripViewBounds.top.toFloat(), emojiStripViewBounds.bottom.toFloat()))
+
+ private fun getSelectedIndexViaMoveEvent(motionEvent: MotionEvent): Int =
+ getSelectedIndexViaMotionEvent(motionEvent, verticalScrubBoundary)
+
+ private fun getSelectedIndexViaMotionEvent(motionEvent: MotionEvent, boundary: Boundary): Int {
+ var selected = -1
+ if (backgroundView.visibility != VISIBLE) {
+ return selected
+ }
+ for (i in emojiViews.indices) {
+ val emojiLeft = segmentSize * i + emojiStripViewBounds.left
+ horizontalEmojiBoundary.update(emojiLeft, emojiLeft + segmentSize)
+ if (horizontalEmojiBoundary.contains(motionEvent.x) && boundary.contains(motionEvent.y)) {
+ selected = i
+ }
+ }
+ if (this.selected != -1 && this.selected != selected) {
+ shrinkView(emojiViews[this.selected])
+ }
+ if (this.selected != selected && selected != -1) {
+ growView(emojiViews[selected])
+ }
+ return selected
+ }
+
+ private fun growView(view: View) {
+ view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
+ view.animate()
+ .scaleY(1.5f)
+ .scaleX(1.5f)
+ .translationY(-selectedVerticalTranslation.toFloat())
+ .setDuration(200)
+ .setInterpolator(INTERPOLATOR)
+ .start()
+ }
+
+ private fun shrinkView(view: View) {
+ view.animate()
+ .scaleX(1.0f)
+ .scaleY(1.0f)
+ .translationY(0f)
+ .setDuration(200)
+ .setInterpolator(INTERPOLATOR)
+ .start()
+ }
+
+ private fun handleUpEvent() {
+ val onReactionSelectedListener = onReactionSelectedListener
+ if (selected != -1 && onReactionSelectedListener != null && backgroundView.visibility == VISIBLE) {
+ if (selected == customEmojiIndex) {
+ onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].tag != null)
+ } else {
+ onReactionSelectedListener.onReactionSelected(messageRecord, recentEmojiPageModel.emoji[selected])
+ }
+ } else {
+ hide()
+ }
+ }
+
+ fun setOnReactionSelectedListener(onReactionSelectedListener: OnReactionSelectedListener?) {
+ this.onReactionSelectedListener = onReactionSelectedListener
+ }
+
+ fun setOnActionSelectedListener(onActionSelectedListener: OnActionSelectedListener?) {
+ this.onActionSelectedListener = onActionSelectedListener
+ }
+
+ fun setOnHideListener(onHideListener: OnHideListener?) {
+ this.onHideListener = onHideListener
+ }
+
+ private fun getOldEmoji(messageRecord: MessageRecord): String? =
+ messageRecord.reactions
+ .filter { it.author == getLocalNumber(context) }
+ .firstOrNull()
+ ?.let(ReactionRecord::emoji)
+
+ private fun getMenuActionItems(message: MessageRecord): List {
+ val items: MutableList = ArrayList()
+
+ // Prepare
+ val containsControlMessage = message.isUpdate
+ val hasText = !message.body.isEmpty()
+ val openGroup = get(context).lokiThreadDatabase().getOpenGroupChat(message.threadId)
+ val recipient = get(context).threadDatabase().getRecipientForThreadId(message.threadId)
+ ?: return emptyList()
+ val userPublicKey = getLocalNumber(context)!!
+ // Select message
+ items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
+ // Reply
+ val canWrite = openGroup == null || openGroup.canWrite
+ if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) {
+ items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
+ }
+ // Copy message text
+ if (!containsControlMessage && hasText) {
+ items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
+ }
+ // Copy Session ID
+ if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
+ items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) })
+ }
+ // Delete message
+ if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
+ items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) }, R.string.AccessibilityId_delete_message, message.subtitle, R.color.destructive)
+ }
+ // Ban user
+ if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
+ items += ActionItem(R.attr.menu_block_icon, R.string.conversation_context__menu_ban_user, { handleActionItemClicked(Action.BAN_USER) })
+ }
+ // Ban and delete all
+ if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
+ items += ActionItem(R.attr.menu_trash_icon, R.string.conversation_context__menu_ban_and_delete_all, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
+ }
+ // Message detail
+ items += ActionItem(R.attr.menu_info_icon, R.string.conversation_context__menu_message_details, { handleActionItemClicked(Action.VIEW_INFO) })
+ // Resend
+ if (message.isFailed) {
+ items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resend_message, { handleActionItemClicked(Action.RESEND) })
+ }
+ // Resync
+ if (message.isSyncFailed) {
+ items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resync_message, { handleActionItemClicked(Action.RESYNC) })
+ }
+ // Save media
+ if (message.isMms && (message as MediaMmsMessageRecord).containsMediaSlide()) {
+ items += ActionItem(R.attr.menu_save_icon, R.string.conversation_context_image__save_attachment, { handleActionItemClicked(Action.DOWNLOAD) }, R.string.AccessibilityId_save_attachment)
+ }
+ backgroundView.visibility = VISIBLE
+ foregroundView.visibility = VISIBLE
+ return items
+ }
+
+ private fun handleActionItemClicked(action: Action) {
+ hideInternal(object : OnHideListener {
+ override fun startHide() {
+ onHideListener?.startHide()
+ }
+
+ override fun onHide() {
+ onHideListener?.onHide()
+ onActionSelectedListener?.onActionSelected(action)
+ }
+ })
+ }
+
+ private fun initAnimators() {
+ val revealDuration = context.resources.getInteger(R.integer.reaction_scrubber_reveal_duration)
+ val revealOffset = context.resources.getInteger(R.integer.reaction_scrubber_reveal_offset)
+ val reveals = emojiViews.mapIndexed { idx: Int, v: EmojiImageView? ->
+ AnimatorInflaterCompat.loadAnimator(context, R.animator.reactions_scrubber_reveal).apply {
+ setTarget(v)
+ startDelay = (idx * animationEmojiStartDelayFactor).toLong()
+ }
+ } + AnimatorInflaterCompat.loadAnimator(context, android.R.animator.fade_in).apply {
+ setTarget(backgroundView)
+ setDuration(revealDuration.toLong())
+ startDelay = revealOffset.toLong()
+ }
+ revealAnimatorSet.interpolator = INTERPOLATOR
+ revealAnimatorSet.playTogether(reveals)
+ }
+
+ private fun newHideAnimatorSet() = AnimatorSet().apply {
+ addListener(object : AnimationCompleteListener() {
+ override fun onAnimationEnd(animation: Animator) {
+ visibility = GONE
+ }
+ })
+ interpolator = INTERPOLATOR
+ playTogether(newHideAnimators())
+ }
+
+ private fun newHideAnimators(): List {
+ val duration = context.resources.getInteger(R.integer.reaction_scrubber_hide_duration).toLong()
+ fun conversationItemAnimator(configure: ObjectAnimator.() -> Unit) = ObjectAnimator().apply {
+ target = conversationItem
+ setDuration(duration)
+ configure()
+ }
+ return emojiViews.map {
+ AnimatorInflaterCompat.loadAnimator(context, R.animator.reactions_scrubber_hide).apply { setTarget(it) }
+ } + AnimatorInflaterCompat.loadAnimator(context, android.R.animator.fade_out).apply {
+ setTarget(backgroundView)
+ setDuration(duration)
+ } + conversationItemAnimator {
+ setProperty(SCALE_X)
+ setFloatValues(1f)
+ } + conversationItemAnimator {
+ setProperty(SCALE_Y)
+ setFloatValues(1f)
+ } + conversationItemAnimator {
+ setProperty(X)
+ setFloatValues(selectedConversationModel.bubbleX)
+ } + conversationItemAnimator {
+ setProperty(Y)
+ setFloatValues(selectedConversationModel.bubbleY - statusBarHeight)
+ } + ValueAnimator.ofArgb(activity.window.statusBarColor, originalStatusBarColor).apply {
+ setDuration(duration)
+ addUpdateListener { animation: ValueAnimator -> WindowUtil.setStatusBarColor(activity.window, animation.animatedValue as Int) }
+ } + ValueAnimator.ofArgb(activity.window.statusBarColor, originalNavigationBarColor).apply {
+ setDuration(duration)
+ addUpdateListener { animation: ValueAnimator -> WindowUtil.setNavigationBarColor(activity.window, animation.animatedValue as Int) }
+ }
+ }
+
+ interface OnHideListener {
+ fun startHide()
+ fun onHide()
+ }
+
+ interface OnReactionSelectedListener {
+ fun onReactionSelected(messageRecord: MessageRecord, emoji: String)
+ fun onCustomReactionSelected(messageRecord: MessageRecord, hasAddedCustomEmoji: Boolean)
+ }
+
+ interface OnActionSelectedListener {
+ fun onActionSelected(action: Action)
+ }
+
+ private class Boundary(private var min: Float = 0f, private var max: Float = 0f) {
+
+ fun update(min: Float, max: Float) {
+ this.min = min
+ this.max = max
+ }
+
+ operator fun contains(value: Float) = if (min < max) {
+ min < value && max > value
+ } else {
+ min > value && max < value
+ }
+ }
+
+ private enum class OverlayState {
+ HIDDEN,
+ UNINITAILIZED,
+ DEADZONE,
+ SCRUB,
+ TAP
+ }
+
+ enum class Action {
+ REPLY,
+ RESEND,
+ RESYNC,
+ DOWNLOAD,
+ COPY_MESSAGE,
+ COPY_SESSION_ID,
+ VIEW_INFO,
+ SELECT,
+ DELETE,
+ BAN_USER,
+ BAN_AND_DELETE_ALL
+ }
+
+ companion object {
+ const val LONG_PRESS_SCALE_FACTOR = 0.95f
+ private val INTERPOLATOR: Interpolator = DecelerateInterpolator()
+ }
+}
+
+private fun Duration.to2partString(): String? =
+ toComponents { days, hours, minutes, seconds, nanoseconds -> listOf(days.days, hours.hours, minutes.minutes, seconds.seconds) }
+ .filter { it.inWholeSeconds > 0L }.take(2).takeIf { it.isNotEmpty() }?.joinToString(" ")
+
+private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
+ get() = if (expiresIn <= 0) {
+ null
+ } else { context ->
+ (expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp)))
+ .coerceAtLeast(0L)
+ .milliseconds
+ .to2partString()
+ ?.let { context.getString(R.string.auto_deletes_in, it) }
+ }
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
index 4d78653abc..1a036eee11 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
@@ -1,25 +1,39 @@
package org.thoughtcrime.securesms.conversation.v2
+import android.content.Context
+
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
+
import com.goterl.lazysodium.utils.KeyPair
+
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+
+import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
+import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.audio.AudioSlidePlayer
+
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
+
import java.util.UUID
class ConversationViewModel(
@@ -29,14 +43,35 @@ class ConversationViewModel(
private val storage: Storage
) : ViewModel() {
- private val _uiState = MutableStateFlow(ConversationUiState())
+ val showSendAfterApprovalText: Boolean
+ get() = recipient?.run { isContactRecipient && !isLocalNumber && !hasApprovedMe() } ?: false
+
+ private val _uiState = MutableStateFlow(ConversationUiState(conversationExists = true))
val uiState: StateFlow = _uiState
- val recipient: Recipient?
- get() = repository.maybeGetRecipientForThreadId(threadId)
+ private var _recipient: RetrieveOnce = RetrieveOnce {
+ repository.maybeGetRecipientForThreadId(threadId)
+ }
+ val expirationConfiguration: ExpirationConfiguration?
+ get() = storage.getExpirationConfiguration(threadId)
+ val recipient: Recipient?
+ get() = _recipient.value
+
+ val blindedRecipient: Recipient?
+ get() = _recipient.value?.let { recipient ->
+ when {
+ recipient.isOpenGroupOutboxRecipient -> recipient
+ recipient.isOpenGroupInboxRecipient -> repository.maybeGetBlindedRecipient(recipient)
+ else -> null
+ }
+ }
+
+ private var _openGroup: RetrieveOnce = RetrieveOnce {
+ storage.getOpenGroup(threadId)
+ }
val openGroup: OpenGroup?
- get() = storage.getOpenGroup(threadId)
+ get() = _openGroup.value
val serverCapabilities: List
get() = openGroup?.let { storage.getServerCapabilities(it.server) } ?: listOf()
@@ -47,12 +82,49 @@ class ConversationViewModel(
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
}
+ val isMessageRequestThread : Boolean
+ get() {
+ val recipient = recipient ?: return false
+ return !recipient.isLocalNumber && !recipient.isGroupRecipient && !recipient.isApproved
+ }
+
+ val canReactToMessages: Boolean
+ // allow reactions if the open group is null (normal conversations) or the open group's capabilities include reactions
+ get() = (openGroup == null || OpenGroupApi.Capability.REACTIONS.name.lowercase() in serverCapabilities)
+
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ repository.recipientUpdateFlow(threadId)
+ .collect { recipient ->
+ if (recipient == null && _uiState.value.conversationExists) {
+ _uiState.update { it.copy(conversationExists = false) }
+ }
+ }
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+
+ // Stop all voice message when exiting this page
+ AudioSlidePlayer.stopAll()
+ }
+
fun saveDraft(text: String) {
- repository.saveDraft(threadId, text)
+ GlobalScope.launch(Dispatchers.IO) {
+ repository.saveDraft(threadId, text)
+ }
}
fun getDraft(): String? {
- return repository.getDraft(threadId)
+ val draft: String? = repository.getDraft(threadId)
+
+ viewModelScope.launch(Dispatchers.IO) {
+ repository.clearDrafts(threadId)
+ }
+
+ return draft
}
fun inviteContacts(contacts: List) {
@@ -78,19 +150,36 @@ class ConversationViewModel(
}
fun deleteLocally(message: MessageRecord) {
+ stopPlayingAudioMessage(message)
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for delete locally action")
repository.deleteLocally(recipient, message)
}
+ /**
+ * Stops audio player if its current playing is the one given in the message.
+ */
+ private fun stopPlayingAudioMessage(message: MessageRecord) {
+ val mmsMessage = message as? MmsMessageRecord ?: return
+ val audioSlide = mmsMessage.slideDeck.audioSlide ?: return
+ AudioSlidePlayer.getInstance()?.takeIf { it.audioSlide == audioSlide }?.stop()
+ }
+
fun setRecipientApproved() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
repository.setApproved(recipient, true)
}
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
- val recipient = recipient ?: return@launch
+ val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.")
+ stopPlayingAudioMessage(message)
+
repository.deleteForEveryone(threadId, recipient, message)
+ .onSuccess {
+ Log.d("Loki", "Deleted message ${message.id} ")
+ stopPlayingAudioMessage(message)
+ }
.onFailure {
+ Log.w("Loki", "FAILED TO delete message ${message.id} ")
showMessage("Couldn't delete message due to error: $it")
}
}
@@ -112,10 +201,15 @@ class ConversationViewModel(
}
}
- fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch {
- repository.banAndDeleteAll(threadId, recipient)
+ fun banAndDeleteAll(messageRecord: MessageRecord) = viewModelScope.launch {
+
+ repository.banAndDeleteAll(threadId, messageRecord.individualRecipient)
.onSuccess {
+ // At this point the server side messages have been successfully deleted..
showMessage("Successfully banned user and deleted all their messages")
+
+ // ..so we can now delete all their messages in this thread from local storage & remove the views.
+ repository.deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord)
}
.onFailure {
showMessage("Couldn't execute request due to error: $it")
@@ -160,6 +254,17 @@ class ConversationViewModel(
return repository.hasReceived(threadId)
}
+ fun updateRecipient() {
+ _recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId))
+ }
+
+ fun hidesInputBar(): Boolean = openGroup?.canWrite != true &&
+ blindedRecipient?.blocksCommunityMessageRequests == true
+
+ fun legacyBannerRecipient(context: Context): Recipient? = recipient?.run {
+ storage.getLastLegacyRecipient(address.serialize())?.let { Recipient.from(context, Address.fromSerialized(it), false) }
+ }
+
@dagger.assisted.AssistedFactory
interface AssistedFactory {
fun create(threadId: Long, edKeyPair: KeyPair?): Factory
@@ -182,7 +287,23 @@ class ConversationViewModel(
data class UiMessage(val id: Long, val message: String)
data class ConversationUiState(
- val isOxenHostedOpenGroup: Boolean = false,
val uiMessages: List = emptyList(),
- val isMessageRequestAccepted: Boolean? = null
+ val isMessageRequestAccepted: Boolean? = null,
+ val conversationExists: Boolean
)
+
+data class RetrieveOnce(val retrieval: () -> T?) {
+ private var triedToRetrieve: Boolean = false
+ private var _value: T? = null
+
+ val value: T?
+ get() {
+ if (triedToRetrieve) { return _value }
+
+ triedToRetrieve = true
+ _value = retrieval()
+ return _value
+ }
+
+ fun updateTo(value: T?) { _value = value }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt
index 66f33cf299..b6212b8542 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt
@@ -69,7 +69,6 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
override fun onStart() {
super.onStart()
val window = dialog?.window ?: return
- val isLightMode = UiModeUtilities.isDayUiMode(requireContext())
- window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
+ window.setDimAmount(0.6f)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
index 27f75701d9..d5e28fb936 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
@@ -1,88 +1,403 @@
package org.thoughtcrime.securesms.conversation.v2
+import android.annotation.SuppressLint
+import android.content.Intent
import android.os.Bundle
-import android.view.View
+import android.view.LayoutInflater
+import android.view.MotionEvent.ACTION_UP
+import androidx.activity.viewModels
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalTextStyle
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.lifecycleScope
+import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
+import com.bumptech.glide.integration.compose.GlideImage
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
import network.loki.messenger.R
-import network.loki.messenger.databinding.ActivityMessageDetailBinding
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.open_groups.OpenGroupApi
-import org.session.libsession.messaging.utilities.SessionId
-import org.session.libsession.messaging.utilities.SodiumUtilities
-import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.ExpirationUtil
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.utilities.IdPrefix
+import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
+import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
-import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
import org.thoughtcrime.securesms.database.Storage
-import org.thoughtcrime.securesms.database.model.MessageRecord
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent
-import org.thoughtcrime.securesms.util.DateUtils
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
+import org.thoughtcrime.securesms.ui.AppTheme
+import org.thoughtcrime.securesms.ui.Avatar
+import org.thoughtcrime.securesms.ui.CarouselNextButton
+import org.thoughtcrime.securesms.ui.CarouselPrevButton
+import org.thoughtcrime.securesms.ui.Cell
+import org.thoughtcrime.securesms.ui.CellNoMargin
+import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
+import org.thoughtcrime.securesms.ui.Divider
+import org.thoughtcrime.securesms.ui.GetString
+import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
+import org.thoughtcrime.securesms.ui.ItemButton
+import org.thoughtcrime.securesms.ui.PreviewTheme
+import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
+import org.thoughtcrime.securesms.ui.TitledText
+import org.thoughtcrime.securesms.ui.blackAlpha40
+import org.thoughtcrime.securesms.ui.colorDestructive
+import org.thoughtcrime.securesms.ui.destructiveButtonColors
import javax.inject.Inject
@AndroidEntryPoint
-class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
- private lateinit var binding: ActivityMessageDetailBinding
- var messageRecord: MessageRecord? = null
+class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
@Inject
lateinit var storage: Storage
- // region Settings
+ private val viewModel: MessageDetailsViewModel by viewModels()
+
companion object {
// Extras
const val MESSAGE_TIMESTAMP = "message_timestamp"
+
+ const val ON_REPLY = 1
+ const val ON_RESEND = 2
+ const val ON_DELETE = 3
}
- // endregion
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
- binding = ActivityMessageDetailBinding.inflate(layoutInflater)
- setContentView(binding.root)
+
title = resources.getString(R.string.conversation_context__menu_message_details)
- val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
- // We only show this screen for messages fail to send,
- // so the author of the messages must be the current user.
- val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
- messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author)
- val threadId = messageRecord!!.threadId
- val openGroup = storage.getOpenGroup(threadId)
- val blindedKey = openGroup?.let { group ->
- val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return@let null
- val blindingEnabled = storage.getServerCapabilities(group.server).contains(OpenGroupApi.Capability.BLIND.name.lowercase())
- if (blindingEnabled) {
- SodiumUtilities.blindedKeyPair(group.publicKey, userEdKeyPair)?.publicKey?.asBytes
- ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
- } else null
- }
- updateContent()
- binding.resendButton.setOnClickListener {
- ResendMessageUtilities.resend(this, messageRecord!!, blindedKey)
- finish()
+
+ viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
+
+ ComposeView(this)
+ .apply { setContent { MessageDetailsScreen() } }
+ .let(::setContentView)
+
+ lifecycleScope.launch {
+ viewModel.eventFlow.collect {
+ when (it) {
+ Event.Finish -> finish()
+ is Event.StartMediaPreview -> startActivity(
+ getPreviewIntent(this@MessageDetailActivity, it.args)
+ )
+ }
+ }
}
}
- fun updateContent() {
- val dateLocale = Locale.getDefault()
- val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
- binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
-
- val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send."
- binding.errorMessage.text = errorMessage
-
- if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
- binding.expiresContainer.visibility = View.GONE
- } else {
- binding.expiresContainer.visibility = View.VISIBLE
- val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted
- val remaining = messageRecord!!.expiresIn - elapsed
-
- val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
- binding.expiresIn.text = duration
+ @Composable
+ private fun MessageDetailsScreen() {
+ val state by viewModel.stateFlow.collectAsState()
+ AppTheme {
+ MessageDetails(
+ state = state,
+ onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
+ onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
+ onDelete = { setResultAndFinish(ON_DELETE) },
+ onClickImage = { viewModel.onClickImage(it) },
+ onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
+ )
}
}
-}
\ No newline at end of file
+
+ private fun setResultAndFinish(code: Int) {
+ Bundle().apply { putLong(MESSAGE_TIMESTAMP, viewModel.timestamp) }
+ .let(Intent()::putExtras)
+ .let { setResult(code, it) }
+
+ finish()
+ }
+}
+
+@SuppressLint("ClickableViewAccessibility")
+@Composable
+fun MessageDetails(
+ state: MessageDetailsState,
+ onReply: (() -> Unit)? = null,
+ onResend: (() -> Unit)? = null,
+ onDelete: () -> Unit = {},
+ onClickImage: (Int) -> Unit = {},
+ onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> }
+) {
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(vertical = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ state.record?.let { message ->
+ AndroidView(
+ modifier = Modifier.padding(horizontal = 32.dp),
+ factory = {
+ ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply {
+ bind(
+ message,
+ thread = state.thread!!,
+ onAttachmentNeedsDownload = onAttachmentNeedsDownload,
+ suppressThumbnails = true
+ )
+
+ setOnTouchListener { _, event ->
+ if (event.actionMasked == ACTION_UP) onContentClick(event)
+ true
+ }
+ }
+ }
+ )
+ }
+ Carousel(state.imageAttachments) { onClickImage(it) }
+ state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
+ CellMetadata(state)
+ CellButtons(
+ onReply,
+ onResend,
+ onDelete,
+ )
+ }
+}
+
+@Composable
+fun CellMetadata(
+ state: MessageDetailsState,
+) {
+ state.apply {
+ if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
+ CellWithPaddingAndMargin {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ TitledText(sent)
+ TitledText(received)
+ TitledErrorText(error)
+ senderInfo?.let {
+ TitledView(state.fromTitle) {
+ Row {
+ sender?.let { Avatar(it) }
+ TitledMonospaceText(it)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun CellButtons(
+ onReply: (() -> Unit)? = null,
+ onResend: (() -> Unit)? = null,
+ onDelete: () -> Unit = {},
+) {
+ Cell {
+ Column {
+ onReply?.let {
+ ItemButton(
+ stringResource(R.string.reply),
+ R.drawable.ic_message_details__reply,
+ onClick = it
+ )
+ Divider()
+ }
+ onResend?.let {
+ ItemButton(
+ stringResource(R.string.resend),
+ R.drawable.ic_message_details__refresh,
+ onClick = it
+ )
+ Divider()
+ }
+ ItemButton(
+ stringResource(R.string.delete),
+ R.drawable.ic_message_details__trash,
+ colors = destructiveButtonColors(),
+ onClick = onDelete
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun Carousel(attachments: List, onClick: (Int) -> Unit) {
+ if (attachments.isEmpty()) return
+
+ val pagerState = rememberPagerState { attachments.size }
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Row {
+ CarouselPrevButton(pagerState)
+ Box(modifier = Modifier.weight(1f)) {
+ CellCarousel(pagerState, attachments, onClick)
+ HorizontalPagerIndicator(pagerState)
+ ExpandButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(8.dp)
+ ) { onClick(pagerState.currentPage) }
+ }
+ CarouselNextButton(pagerState)
+ }
+ attachments.getOrNull(pagerState.currentPage)?.fileDetails?.let { FileDetails(it) }
+ }
+}
+
+@OptIn(
+ ExperimentalFoundationApi::class,
+ ExperimentalGlideComposeApi::class
+)
+@Composable
+private fun CellCarousel(
+ pagerState: PagerState,
+ attachments: List,
+ onClick: (Int) -> Unit
+) {
+ CellNoMargin {
+ HorizontalPager(state = pagerState) { i ->
+ GlideImage(
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .aspectRatio(1f)
+ .clickable { onClick(i) },
+ model = attachments[i].uri,
+ contentDescription = attachments[i].fileName ?: stringResource(id = R.string.image)
+ )
+ }
+ }
+}
+
+@Composable
+fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
+ Surface(
+ shape = CircleShape,
+ color = blackAlpha40,
+ modifier = modifier,
+ contentColor = Color.White,
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_expand),
+ contentDescription = stringResource(id = R.string.expand),
+ modifier = Modifier.clickable { onClick() },
+ )
+ }
+}
+
+
+@Preview
+@Composable
+fun PreviewMessageDetails(
+ @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
+) {
+ PreviewTheme(themeResId) {
+ MessageDetails(
+ state = MessageDetailsState(
+ nonImageAttachmentFileDetails = listOf(
+ TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
+ TitledText(R.string.message_details_header__file_type, "image/png"),
+ TitledText(R.string.message_details_header__file_size, "195.6kB"),
+ TitledText(R.string.message_details_header__resolution, "342x312"),
+ ),
+ sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"),
+ received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"),
+ error = TitledText(R.string.message_details_header__error, "Message failed to send"),
+ senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
+ )
+ )
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun FileDetails(fileDetails: List) {
+ if (fileDetails.isEmpty()) return
+
+ CellWithPaddingAndMargin(padding = 0.dp) {
+ FlowRow(
+ modifier = Modifier.padding(vertical = 24.dp, horizontal = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ fileDetails.forEach {
+ BoxWithConstraints {
+ TitledText(
+ it,
+ modifier = Modifier
+ .widthIn(min = maxWidth.div(2))
+ .padding(horizontal = 12.dp)
+ .width(IntrinsicSize.Max)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TitledErrorText(titledText: TitledText?) {
+ TitledText(
+ titledText,
+ valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
+ )
+}
+
+@Composable
+fun TitledMonospaceText(titledText: TitledText?) {
+ TitledText(
+ titledText,
+ valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
+ )
+}
+
+@Composable
+fun TitledText(
+ titledText: TitledText?,
+ modifier: Modifier = Modifier,
+ valueStyle: TextStyle = LocalTextStyle.current,
+) {
+ titledText?.apply {
+ TitledView(title, modifier) {
+ Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth())
+ }
+ }
+}
+
+@Composable
+fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Title(title)
+ content()
+ }
+}
+
+@Composable
+fun Title(title: GetString) {
+ Text(title.string(), fontWeight = FontWeight.Bold)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
new file mode 100644
index 0000000000..ba153a6b36
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
@@ -0,0 +1,174 @@
+package org.thoughtcrime.securesms.conversation.v2
+
+import android.net.Uri
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import network.loki.messenger.R
+import org.session.libsession.messaging.jobs.AttachmentDownloadJob
+import org.session.libsession.messaging.jobs.JobQueue
+import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
+import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
+import org.session.libsession.utilities.Util
+import org.session.libsession.utilities.recipients.Recipient
+import org.thoughtcrime.securesms.MediaPreviewArgs
+import org.thoughtcrime.securesms.database.AttachmentDatabase
+import org.thoughtcrime.securesms.database.LokiMessageDatabase
+import org.thoughtcrime.securesms.database.MmsSmsDatabase
+import org.thoughtcrime.securesms.database.ThreadDatabase
+import org.thoughtcrime.securesms.database.model.MessageRecord
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord
+import org.thoughtcrime.securesms.mms.ImageSlide
+import org.thoughtcrime.securesms.mms.Slide
+import org.thoughtcrime.securesms.repository.ConversationRepository
+import org.thoughtcrime.securesms.ui.GetString
+import org.thoughtcrime.securesms.ui.TitledText
+import java.util.Date
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+@HiltViewModel
+class MessageDetailsViewModel @Inject constructor(
+ private val attachmentDb: AttachmentDatabase,
+ private val lokiMessageDatabase: LokiMessageDatabase,
+ private val mmsSmsDatabase: MmsSmsDatabase,
+ private val threadDb: ThreadDatabase,
+ private val repository: ConversationRepository,
+) : ViewModel() {
+
+ private var job: Job? = null
+
+ private val state = MutableStateFlow(MessageDetailsState())
+ val stateFlow = state.asStateFlow()
+
+ private val event = Channel()
+ val eventFlow = event.receiveAsFlow()
+
+ var timestamp: Long = 0L
+ set(value) {
+ job?.cancel()
+
+ field = value
+ val record = mmsSmsDatabase.getMessageForTimestamp(timestamp)
+
+ if (record == null) {
+ viewModelScope.launch { event.send(Event.Finish) }
+ return
+ }
+
+ val mmsRecord = record as? MmsMessageRecord
+
+ job = viewModelScope.launch {
+ repository.changes(record.threadId)
+ .filter { mmsSmsDatabase.getMessageForTimestamp(value) == null }
+ .collect { event.send(Event.Finish) }
+ }
+
+ state.value = record.run {
+ val slides = mmsRecord?.slideDeck?.slides ?: emptyList()
+
+ MessageDetailsState(
+ attachments = slides.map(::Attachment),
+ record = record,
+ sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) },
+ received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) },
+ error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) },
+ senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
+ sender = individualRecipient,
+ thread = threadDb.getRecipientForThreadId(threadId)!!,
+ )
+ }
+ }
+
+ private val Slide.details: List
+ get() = listOfNotNull(
+ fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) },
+ TitledText(R.string.message_details_header__file_type, asAttachment().contentType),
+ TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)),
+ takeIf { it is ImageSlide }
+ ?.let(Slide::asAttachment)
+ ?.run { "${width}x$height" }
+ ?.let { TitledText(R.string.message_details_header__resolution, it) },
+ attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) },
+ )
+
+ private fun AttachmentDatabase.duration(slide: Slide): String? =
+ slide.takeIf { it.hasAudio() }
+ ?.run { asAttachment() as? DatabaseAttachment }
+ ?.run { getAttachmentAudioExtras(attachmentId)?.durationMs }
+ ?.takeIf { it > 0 }
+ ?.let {
+ String.format(
+ "%01d:%02d",
+ TimeUnit.MILLISECONDS.toMinutes(it),
+ TimeUnit.MILLISECONDS.toSeconds(it) % 60
+ )
+ }
+
+ fun Attachment(slide: Slide): Attachment =
+ Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
+
+ fun onClickImage(index: Int) {
+ val state = state.value
+ val mmsRecord = state.mmsRecord ?: return
+ val slide = mmsRecord.slideDeck.slides[index] ?: return
+ // only open to downloaded images
+ if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
+ // Restart download here (on IO thread)
+ (slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
+ onAttachmentNeedsDownload(attachment.attachmentId.rowId, state.mmsRecord.getId())
+ }
+ }
+
+ if (slide.isInProgress) return
+
+ viewModelScope.launch {
+ MediaPreviewArgs(slide, state.mmsRecord, state.thread)
+ .let(Event::StartMediaPreview)
+ .let { event.send(it) }
+ }
+ }
+
+ fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) {
+ viewModelScope.launch(Dispatchers.IO) {
+ JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
+ }
+ }
+}
+
+data class MessageDetailsState(
+ val attachments: List = emptyList(),
+ val imageAttachments: List = attachments.filter { it.hasImage },
+ val nonImageAttachmentFileDetails: List? = attachments.firstOrNull { !it.hasImage }?.fileDetails,
+ val record: MessageRecord? = null,
+ val mmsRecord: MmsMessageRecord? = record as? MmsMessageRecord,
+ val sent: TitledText? = null,
+ val received: TitledText? = null,
+ val error: TitledText? = null,
+ val senderInfo: TitledText? = null,
+ val sender: Recipient? = null,
+ val thread: Recipient? = null,
+) {
+ val fromTitle = GetString(R.string.message_details_header__from)
+ val canReply = record?.isOpenGroupInvitation != true
+}
+
+data class Attachment(
+ val fileDetails: List,
+ val fileName: String?,
+ val uri: Uri?,
+ val hasImage: Boolean
+)
+
+sealed class Event {
+ object Finish: Event()
+ data class StartMediaPreview(val args: MediaPreviewArgs): Event()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt
index 28c86b3311..54deea1c8d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt
@@ -60,8 +60,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
override fun onStart() {
super.onStart()
val window = dialog?.window ?: return
- val isLightMode = UiModeUtilities.isDayUiMode(requireContext())
- window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
+ window.setDimAmount(0.6f)
}
override fun onClick(v: View?) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/WindowUtil.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/WindowUtil.java
index 4bff4e76aa..6083bb267c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/WindowUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/WindowUtil.java
@@ -38,14 +38,10 @@ public final class WindowUtil {
}
public static void setNavigationBarColor(@NonNull Window window, @ColorInt int color) {
- if (Build.VERSION.SDK_INT < 21) return;
-
window.setNavigationBarColor(color);
}
public static void setLightStatusBarFromTheme(@NonNull Activity activity) {
- if (Build.VERSION.SDK_INT < 23) return;
-
final boolean isLightStatusBar = ThemeUtil.getThemedBoolean(activity, android.R.attr.windowLightStatusBar);
if (isLightStatusBar) setLightStatusBar(activity.getWindow());
@@ -53,20 +49,14 @@ public final class WindowUtil {
}
public static void clearLightStatusBar(@NonNull Window window) {
- if (Build.VERSION.SDK_INT < 23) return;
-
clearSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
public static void setLightStatusBar(@NonNull Window window) {
- if (Build.VERSION.SDK_INT < 23) return;
-
setSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
public static void setStatusBarColor(@NonNull Window window, @ColorInt int color) {
- if (Build.VERSION.SDK_INT < 21) return;
-
window.setStatusBarColor(color);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt
index 8f0ddd8bef..d76e6f2b3d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt
@@ -7,55 +7,40 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
-import android.widget.FrameLayout
+import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.view.children
import androidx.core.view.isVisible
import network.loki.messenger.R
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
-import org.session.libsession.messaging.jobs.AttachmentDownloadJob
-import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask
-import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
+import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.ActivityDispatcher
-class AlbumThumbnailView : FrameLayout {
-
- private lateinit var binding: AlbumThumbnailViewBinding
-
+class AlbumThumbnailView : RelativeLayout {
companion object {
const val MAX_ALBUM_DISPLAY_SIZE = 3
}
+ private val binding: AlbumThumbnailViewBinding by lazy { AlbumThumbnailViewBinding.bind(this) }
+
// region Lifecycle
- constructor(context: Context) : super(context) {
- initialize()
- }
-
- constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
- initialize()
- }
-
- constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
- initialize()
- }
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private val cornerMask by lazy { CornerMask(this) }
private var slides: List = listOf()
private var slideSize: Int = 0
- private fun initialize() {
- binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true)
- }
-
- override fun dispatchDraw(canvas: Canvas?) {
+ override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
cornerMask.mask(canvas)
}
@@ -63,26 +48,25 @@ class AlbumThumbnailView : FrameLayout {
// region Interaction
- fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient) {
+ fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient, onAttachmentNeedsDownload: (Long, Long) -> Unit) {
val rawXInt = event.rawX.toInt()
val rawYInt = event.rawY.toInt()
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
val testRect = Rect()
// test each album child
- binding.albumCellContainer.findViewById(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
+ binding.albumCellContainer.findViewById(R.id.album_thumbnail_root)?.children?.forEachIndexed forEach@{ index, child ->
child.getGlobalVisibleRect(testRect)
if (testRect.contains(eventRect)) {
// hit intersects with this particular child
- val slide = slides.getOrNull(index) ?: return
+ val slide = slides.getOrNull(index) ?: return@forEach
// only open to downloaded images
if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
- // restart download here
+ // Restart download here (on IO thread)
(slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
- val attachmentId = attachment.attachmentId.rowId
- JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mms.getId()))
+ onAttachmentNeedsDownload(attachment.attachmentId.rowId, mms.getId())
}
}
- if (slide.isInProgress) return
+ if (slide.isInProgress) return@forEach
ActivityDispatcher.get(context)?.dispatchIntent { context ->
MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient)
@@ -133,7 +117,7 @@ class AlbumThumbnailView : FrameLayout {
else -> R.layout.album_thumbnail_3 // three stacked with additional text
}
- fun getThumbnailView(position: Int): KThumbnailView = when (position) {
+ fun getThumbnailView(position: Int): ThumbnailView = when (position) {
0 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
1 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
2 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java
deleted file mode 100644
index 6765232c77..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package org.thoughtcrime.securesms.conversation.v2.components;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.session.libsession.utilities.Util;
-
-import java.lang.ref.WeakReference;
-import java.util.concurrent.TimeUnit;
-
-import network.loki.messenger.R;
-
-public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImageView {
-
- private long startedAt;
- private long expiresIn;
-
- private boolean visible = false;
- private boolean stopped = true;
-
- private final int[] frames = new int[]{ R.drawable.timer00,
- R.drawable.timer05,
- R.drawable.timer10,
- R.drawable.timer15,
- R.drawable.timer20,
- R.drawable.timer25,
- R.drawable.timer30,
- R.drawable.timer35,
- R.drawable.timer40,
- R.drawable.timer45,
- R.drawable.timer50,
- R.drawable.timer55,
- R.drawable.timer60 };
-
- public ExpirationTimerView(Context context) {
- super(context);
- }
-
- public ExpirationTimerView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- public ExpirationTimerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public void setExpirationTime(long startedAt, long expiresIn) {
- this.startedAt = startedAt;
- this.expiresIn = expiresIn;
- setPercentComplete(calculateProgress(this.startedAt, this.expiresIn));
- }
-
- public void setPercentComplete(float percentage) {
- float percentFull = 1 - percentage;
- int frame = (int) Math.ceil(percentFull * (frames.length - 1));
-
- frame = Math.max(0, Math.min(frame, frames.length - 1));
- setImageResource(frames[frame]);
- }
-
- public void startAnimation() {
- synchronized (this) {
- visible = true;
- if (!stopped) return;
- else stopped = false;
- }
-
- Util.runOnMainDelayed(new AnimationUpdateRunnable(this), calculateAnimationDelay(this.startedAt, this.expiresIn));
- }
-
- public void stopAnimation() {
- synchronized (this) {
- visible = false;
- }
- }
-
- private float calculateProgress(long startedAt, long expiresIn) {
- long progressed = System.currentTimeMillis() - startedAt;
- float percentComplete = (float)progressed / (float)expiresIn;
-
- return Math.max(0, Math.min(percentComplete, 1));
- }
-
- private long calculateAnimationDelay(long startedAt, long expiresIn) {
- long progressed = System.currentTimeMillis() - startedAt;
- long remaining = expiresIn - progressed;
-
- if (remaining <= 0) {
- return 0;
- } else if (remaining < TimeUnit.SECONDS.toMillis(30)) {
- return 1000;
- } else {
- return 5000;
- }
- }
-
- private static class AnimationUpdateRunnable implements Runnable {
-
- private final WeakReference expirationTimerViewReference;
-
- private AnimationUpdateRunnable(@NonNull ExpirationTimerView expirationTimerView) {
- this.expirationTimerViewReference = new WeakReference<>(expirationTimerView);
- }
-
- @Override
- public void run() {
- ExpirationTimerView timerView = expirationTimerViewReference.get();
- if (timerView == null) return;
-
- long nextUpdate = timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn);
- synchronized (timerView) {
- if (timerView.visible) {
- timerView.setExpirationTime(timerView.startedAt, timerView.expiresIn);
- } else {
- timerView.stopped = true;
- return;
- }
- if (nextUpdate <= 0) {
- timerView.stopped = true;
- return;
- }
- }
- Util.runOnMainDelayed(this, nextUpdate);
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.kt
new file mode 100644
index 0000000000..d173dacfef
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.kt
@@ -0,0 +1,61 @@
+package org.thoughtcrime.securesms.conversation.v2.components
+
+import android.content.Context
+import android.graphics.drawable.AnimationDrawable
+import android.util.AttributeSet
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat
+import network.loki.messenger.R
+import org.session.libsession.snode.SnodeAPI.nowWithOffset
+import kotlin.math.round
+
+class ExpirationTimerView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : AppCompatImageView(context, attrs, defStyleAttr) {
+ private val frames = intArrayOf(
+ R.drawable.timer00,
+ R.drawable.timer05,
+ R.drawable.timer10,
+ R.drawable.timer15,
+ R.drawable.timer20,
+ R.drawable.timer25,
+ R.drawable.timer30,
+ R.drawable.timer35,
+ R.drawable.timer40,
+ R.drawable.timer45,
+ R.drawable.timer50,
+ R.drawable.timer55,
+ R.drawable.timer60
+ )
+
+ fun setTimerIcon() {
+ setExpirationTime(0L, 0L)
+ }
+
+ fun setExpirationTime(startedAt: Long, expiresIn: Long) {
+ if (expiresIn == 0L) {
+ setImageResource(R.drawable.timer55)
+ return
+ }
+
+ if (startedAt == 0L) {
+ // timer has not started
+ setImageResource(R.drawable.timer60)
+ return
+ }
+
+ val elapsedTime = nowWithOffset - startedAt
+ val remainingTime = expiresIn - elapsedTime
+ val remainingPercent = (remainingTime / expiresIn.toFloat()).coerceIn(0f, 1f)
+
+ val frameCount = round(frames.size * remainingPercent).toInt().coerceIn(1, frames.size)
+ val frameTime = round(remainingTime / frameCount.toFloat()).toInt()
+
+ AnimationDrawable().apply {
+ frames.take(frameCount).reversed().forEach { addFrame(ContextCompat.getDrawable(context, it)!!, frameTime) }
+ isOneShot = true
+ }.also(::setImageDrawable).apply(AnimationDrawable::start)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt
index c1fce3f50b..66164f100f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt
@@ -23,7 +23,7 @@ class LinkPreviewDraftView : LinearLayout {
// Start out with the loader showing and the content view hidden
binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
binding.linkPreviewDraftContainer.isVisible = false
- binding.thumbnailImageView.clipToOutline = true
+ binding.thumbnailImageView.root.clipToOutline = true
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
}
@@ -31,10 +31,10 @@ class LinkPreviewDraftView : LinearLayout {
// Hide the loader and show the content view
binding.linkPreviewDraftContainer.isVisible = true
binding.linkPreviewDraftLoader.isVisible = false
- binding.thumbnailImageView.radius = toPx(4, resources)
+ binding.thumbnailImageView.root.radius = toPx(4, resources)
if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail
- binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
+ binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, null)
}
binding.linkPreviewDraftTitleTextView.text = linkPreview.title
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
index 834b77eccb..d544263915 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
@@ -28,11 +28,10 @@ class MentionCandidateView : LinearLayout {
private fun update() = with(binding) {
mentionCandidateNameTextView.text = mentionCandidate.displayName
- profilePictureView.root.publicKey = mentionCandidate.publicKey
- profilePictureView.root.displayName = mentionCandidate.displayName
- profilePictureView.root.additionalPublicKey = null
- profilePictureView.root.glide = glide!!
- profilePictureView.root.update()
+ profilePictureView.publicKey = mentionCandidate.publicKey
+ profilePictureView.displayName = mentionCandidate.displayName
+ profilePictureView.additionalPublicKey = null
+ profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java
deleted file mode 100644
index 826cfe7b3a..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package org.thoughtcrime.securesms.conversation.v2.components;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import androidx.annotation.Nullable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-
-import network.loki.messenger.R;
-
-public class TypingIndicatorView extends LinearLayout {
- private boolean isActive;
- private long startTime;
-
- private static final long CYCLE_DURATION = 1500;
- private static final long DOT_DURATION = 600;
- private static final float MIN_ALPHA = 0.4f;
- private static final float MIN_SCALE = 0.75f;
-
- private View dot1;
- private View dot2;
- private View dot3;
-
- public TypingIndicatorView(Context context) {
- super(context);
- initialize(null);
- }
-
- public TypingIndicatorView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- initialize(attrs);
- }
-
- private void initialize(@Nullable AttributeSet attrs) {
- inflate(getContext(), R.layout.view_typing_indicator, this);
-
- setWillNotDraw(false);
-
- dot1 = findViewById(R.id.typing_dot1);
- dot2 = findViewById(R.id.typing_dot2);
- dot3 = findViewById(R.id.typing_dot3);
-
- if (attrs != null) {
- TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0);
- int tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE);
- typedArray.recycle();
-
- dot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
- dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
- dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- if (!isActive) {
- super.onDraw(canvas);
- return;
- }
-
- long timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION;
-
- render(dot1, timeInCycle, 0);
- render(dot2, timeInCycle, 150);
- render(dot3, timeInCycle, 300);
-
- super.onDraw(canvas);
- postInvalidate();
- }
-
- private void render(View dot, long timeInCycle, long start) {
- long end = start + DOT_DURATION;
- long peak = start + (DOT_DURATION / 2);
-
- if (timeInCycle < start || timeInCycle > end) {
- renderDefault(dot);
- } else if (timeInCycle < peak) {
- renderFadeIn(dot, timeInCycle, start);
- } else {
- renderFadeOut(dot, timeInCycle, peak);
- }
- }
-
- private void renderDefault(View dot) {
- dot.setAlpha(MIN_ALPHA);
- dot.setScaleX(MIN_SCALE);
- dot.setScaleY(MIN_SCALE);
- }
-
- private void renderFadeIn(View dot, long timeInCycle, long fadeInStart) {
- float percent = (float) (timeInCycle - fadeInStart) / 300;
- dot.setAlpha(MIN_ALPHA + (1 - MIN_ALPHA) * percent);
- dot.setScaleX(MIN_SCALE + (1 - MIN_SCALE) * percent);
- dot.setScaleY(MIN_SCALE + (1 - MIN_SCALE) * percent);
- }
-
- private void renderFadeOut(View dot, long timeInCycle, long fadeOutStart) {
- float percent = (float) (timeInCycle - fadeOutStart) / 300;
- dot.setAlpha(1 - (1 - MIN_ALPHA) * percent);
- dot.setScaleX(1 - (1 - MIN_SCALE) * percent);
- dot.setScaleY(1 - (1 - MIN_SCALE) * percent);
- }
-
- public void startAnimation() {
- isActive = true;
- startTime = System.currentTimeMillis();
-
- postInvalidate();
- }
-
- public void stopAnimation() {
- isActive = false;
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.kt
new file mode 100644
index 0000000000..d1310bffba
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.kt
@@ -0,0 +1,105 @@
+package org.thoughtcrime.securesms.conversation.v2.components
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import network.loki.messenger.R
+import network.loki.messenger.databinding.ViewTypingIndicatorBinding
+
+class TypingIndicatorView : LinearLayout {
+ companion object {
+ private const val CYCLE_DURATION: Long = 1500
+ private const val DOT_DURATION: Long = 600
+ private const val MIN_ALPHA = 0.4f
+ private const val MIN_SCALE = 0.75f
+ }
+
+ private val binding: ViewTypingIndicatorBinding by lazy {
+ val binding = ViewTypingIndicatorBinding.bind(this)
+
+ if (tint != -1) {
+ binding.typingDot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
+ binding.typingDot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
+ binding.typingDot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
+ }
+
+ return@lazy binding
+ }
+
+ private var isActive = false
+ private var startTime: Long = 0
+ private var tint: Int = -1
+
+ constructor(context: Context) : super(context) { initialize(null) }
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
+
+ private fun initialize(attrs: AttributeSet?) {
+ setWillNotDraw(false)
+
+ if (attrs != null) {
+ val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0)
+ this.tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE)
+ typedArray.recycle()
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ if (!isActive) {
+ super.onDraw(canvas)
+ return
+ }
+ val timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION
+ render(binding.typingDot1, timeInCycle, 0)
+ render(binding.typingDot2, timeInCycle, 150)
+ render(binding.typingDot3, timeInCycle, 300)
+ super.onDraw(canvas)
+ postInvalidate()
+ }
+
+ private fun render(dot: View?, timeInCycle: Long, start: Long) {
+ val end = start + DOT_DURATION
+ val peak = start + DOT_DURATION / 2
+ if (timeInCycle < start || timeInCycle > end) {
+ renderDefault(dot)
+ } else if (timeInCycle < peak) {
+ renderFadeIn(dot, timeInCycle, start)
+ } else {
+ renderFadeOut(dot, timeInCycle, peak)
+ }
+ }
+
+ private fun renderDefault(dot: View?) {
+ dot!!.alpha = MIN_ALPHA
+ dot.scaleX = MIN_SCALE
+ dot.scaleY = MIN_SCALE
+ }
+
+ private fun renderFadeIn(dot: View?, timeInCycle: Long, fadeInStart: Long) {
+ val percent = (timeInCycle - fadeInStart).toFloat() / 300
+ dot!!.alpha = MIN_ALPHA + (1 - MIN_ALPHA) * percent
+ dot.scaleX = MIN_SCALE + (1 - MIN_SCALE) * percent
+ dot.scaleY = MIN_SCALE + (1 - MIN_SCALE) * percent
+ }
+
+ private fun renderFadeOut(dot: View?, timeInCycle: Long, fadeOutStart: Long) {
+ val percent = (timeInCycle - fadeOutStart).toFloat() / 300
+ dot!!.alpha = 1 - (1 - MIN_ALPHA) * percent
+ dot.scaleX = 1 - (1 - MIN_SCALE) * percent
+ dot.scaleY = 1 - (1 - MIN_SCALE) * percent
+ }
+
+ fun startAnimation() {
+ isActive = true
+ startTime = System.currentTimeMillis()
+ postInvalidate()
+ }
+
+ fun stopAnimation() {
+ isActive = false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt
index 768d49146e..3077d227e3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt
@@ -19,7 +19,7 @@ class TypingIndicatorViewContainer : LinearLayout {
}
fun setTypists(typists: List) {
- if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return }
- binding.typingIndicator.startAnimation()
+ if (typists.isEmpty()) { binding.typingIndicator.root.stopAnimation(); return }
+ binding.typingIndicator.root.startAnimation()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt
index 39ca7c6913..c0ff1cbb1d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt
@@ -1,41 +1,42 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
+import android.app.Dialog
+import android.content.Context
import android.graphics.Typeface
+import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
-import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
import network.loki.messenger.R
-import network.loki.messenger.databinding.DialogBlockedBinding
+import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
/** Shown upon sending a message to a user that's blocked. */
-class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
+class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() {
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext()))
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
val sessionID = recipient.address.toString()
val contact = contactDB.getContactWithSessionID(sessionID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
- val title = resources.getString(R.string.dialog_blocked_title, name)
- binding.blockedTitleTextView.text = title
+
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- binding.blockedExplanationTextView.text = spannable
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.unblockButton.setOnClickListener { unblock() }
- builder.setView(binding.root)
+
+ title(resources.getString(R.string.dialog_blocked_title, name))
+ text(spannable)
+ button(R.string.ConversationActivity_unblock) { unblock() }
+ cancelButton { dismiss() }
}
private fun unblock() {
- DatabaseComponent.get(requireContext()).recipientDatabase().setBlocked(recipient, false)
+ MessagingModuleConfiguration.shared.storage.setBlocked(listOf(recipient), false)
dismiss()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt
index 42cca1ad3e..5edd63f100 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt
@@ -1,19 +1,19 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
+import android.app.Dialog
import android.graphics.Typeface
+import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
-import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
-import network.loki.messenger.databinding.DialogDownloadBinding
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.utilities.recipients.Recipient
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import javax.inject.Inject
@@ -21,25 +21,24 @@ import javax.inject.Inject
/** Shown when receiving media from a contact for the first time, to confirm that
* they are to be trusted and files sent by them are to be downloaded. */
@AndroidEntryPoint
-class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
+class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
@Inject lateinit var contactDB: SessionContactDatabase
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogDownloadBinding.inflate(LayoutInflater.from(requireContext()))
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
val sessionID = recipient.address.toString()
val contact = contactDB.getContactWithSessionID(sessionID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
- val title = resources.getString(R.string.dialog_download_title, name)
- binding.downloadTitleTextView.text = title
+ title(resources.getString(R.string.dialog_download_title, name))
+
val explanation = resources.getString(R.string.dialog_download_explanation, name)
val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- binding.downloadExplanationTextView.text = spannable
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.downloadButton.setOnClickListener { trust() }
- builder.setView(binding.root)
+ text(spannable)
+
+ button(R.string.dialog_download_button_title, R.string.AccessibilityId_download_media) { trust() }
+ cancelButton { dismiss() }
}
private fun trust() {
@@ -50,4 +49,4 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
JobQueue.shared.resumePendingJobs(AttachmentDownloadJob.KEY)
dismiss()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
index 444c389e04..a886e89192 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
@@ -1,46 +1,42 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
+import android.app.Dialog
import android.graphics.Typeface
+import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
-import android.view.LayoutInflater
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.DialogFragment
import network.loki.messenger.R
-import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.ThreadUtils
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
/** Shown upon tapping an open group invitation. */
-class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
+class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogJoinOpenGroupBinding.inflate(LayoutInflater.from(requireContext()))
- val title = resources.getString(R.string.dialog_join_open_group_title, name)
- binding.joinOpenGroupTitleTextView.text = title
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ title(resources.getString(R.string.dialog_join_open_group_title, name))
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- binding.joinOpenGroupExplanationTextView.text = spannable
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.joinButton.setOnClickListener { join() }
- builder.setView(binding.root)
+ text(spannable)
+ cancelButton { dismiss() }
+ button(R.string.open_group_invitation_view__join_accessibility_description) { join() }
}
private fun join() {
val openGroup = OpenGroupUrlParser.parseUrl(url)
- val activity = requireContext() as AppCompatActivity
+ val activity = requireActivity()
ThreadUtils.queue {
try {
- OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
- MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server)
+ openGroup.apply { OpenGroupManager.add(server, room, serverPublicKey, activity) }
+ MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
} catch (e: Exception) {
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
@@ -48,4 +44,4 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
}
dismiss()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt
index a16ca86f79..996dd41f94 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt
@@ -1,20 +1,21 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
-import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
-import network.loki.messenger.databinding.DialogLinkPreviewBinding
+import android.app.Dialog
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import org.thoughtcrime.securesms.createSessionDialog
/** Shown the first time the user inputs a URL that could generate a link preview, to
* let them know that Session offers the ability to send and receive link previews. */
-class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
+class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogLinkPreviewBinding.inflate(LayoutInflater.from(requireContext()))
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.enableLinkPreviewsButton.setOnClickListener { enable() }
- builder.setView(binding.root)
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ title(R.string.dialog_link_preview_title)
+ text(R.string.dialog_link_preview_explanation)
+ button(R.string.dialog_link_preview_enable_button_title) { enable() }
+ cancelButton { dismiss() }
}
private fun enable() {
@@ -22,4 +23,4 @@ class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
dismiss()
onEnabled()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt
index f51261d499..6abb0814d6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt
@@ -1,22 +1,23 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
-import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
-import network.loki.messenger.databinding.DialogSendSeedBinding
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import android.app.Dialog
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.createSessionDialog
/** Shown if the user is about to send their recovery phrase to someone. */
-class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
+class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() {
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext()))
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.sendSeedButton.setOnClickListener { send() }
- builder.setView(binding.root)
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ title(R.string.dialog_send_seed_title)
+ text(R.string.dialog_send_seed_explanation)
+ button(R.string.dialog_send_seed_send_button_title) { send() }
+ cancelButton()
}
private fun send() {
proceed?.invoke()
dismiss()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt
index 7ac70b843a..3544f11b10 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt
@@ -37,6 +37,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
private val vMargin by lazy { toDp(4, resources) }
private val minHeight by lazy { toPx(56, resources) }
private var linkPreviewDraftView: LinkPreviewDraftView? = null
+ private var quoteView: QuoteView? = null
var delegate: InputBarDelegate? = null
var additionalContentHeight = 0
var quote: MessageRecord? = null
@@ -57,9 +58,9 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
val attachmentButtonsContainerHeight: Int
get() = binding.attachmentsButtonContainer.height
- private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
- private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) }
- private val sendButton by lazy { InputBarButton(context, R.drawable.ic_arrow_up, true) }
+ private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)} }
+ private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)} }
+ private val sendButton by lazy { InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_button)} }
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
@@ -98,7 +99,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_NONE
binding.inputBarEditText.inputType =
binding.inputBarEditText.inputType or
- InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
}
val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0
binding.inputBarEditText.imeOptions = binding.inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled
@@ -138,53 +139,66 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
delegate?.startRecordingVoiceMessage()
}
- // Drafting quotes and drafting link previews is mutually exclusive, i.e. you can't draft
- // a quote and a link preview at the same time.
-
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
- quote = message
- linkPreview = null
- linkPreviewDraftView = null
- binding.inputBarAdditionalContentContainer.removeAllViews()
+ quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
- // inflate quoteview with typed array here
+ quote = message
+
+ // If we already have a link preview View then clear the 'additional content' layout so that
+ // our quote View is always the first element (i.e., at the top of the reply).
+ if (linkPreview != null && linkPreviewDraftView != null) {
+ binding.inputBarAdditionalContentContainer.removeAllViews()
+ }
+
+ // Inflate quote View with typed array here
val layout = LayoutInflater.from(context).inflate(R.layout.view_quote_draft, binding.inputBarAdditionalContentContainer, false)
- val quoteView = layout.findViewById(R.id.mainQuoteViewContainer)
- quoteView.delegate = this
- binding.inputBarAdditionalContentContainer.addView(layout)
- val attachments = (message as? MmsMessageRecord)?.slideDeck
- val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
- quoteView.bind(sender, message.body, attachments,
- thread, true, message.isOpenGroupInvitation, message.threadId, false, glide)
+ quoteView = layout.findViewById(R.id.mainQuoteViewContainer).also {
+ it.delegate = this
+ binding.inputBarAdditionalContentContainer.addView(layout)
+ val attachments = (message as? MmsMessageRecord)?.slideDeck
+ val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
+ it.bind(sender, message.body, attachments, thread, true, message.isOpenGroupInvitation, message.threadId, false, glide)
+ }
+
+ // Before we request a layout update we'll add back any LinkPreviewDraftView that might
+ // exist - as this goes into the LinearLayout second it will be below the quote View.
+ if (linkPreview != null && linkPreviewDraftView != null) {
+ binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
+ }
requestLayout()
}
override fun cancelQuoteDraft() {
+ binding.inputBarAdditionalContentContainer.removeView(quoteView)
quote = null
- binding.inputBarAdditionalContentContainer.removeAllViews()
+ quoteView = null
requestLayout()
}
fun draftLinkPreview() {
- quote = null
- binding.inputBarAdditionalContentContainer.removeAllViews()
- val linkPreviewDraftView = LinkPreviewDraftView(context)
- linkPreviewDraftView.delegate = this
- this.linkPreviewDraftView = linkPreviewDraftView
+ // As `draftLinkPreview` is called before `updateLinkPreview` when we modify a URI in a
+ // message we'll bail early if a link preview View already exists and just let
+ // `updateLinkPreview` get called to update the existing View.
+ if (linkPreview != null && linkPreviewDraftView != null) return
+ linkPreviewDraftView?.let(binding.inputBarAdditionalContentContainer::removeView)
+ linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this }
+
+ // Add the link preview View. Note: If there's already a quote View in the 'additional
+ // content' container then this preview View will be added after / below it - which is fine.
binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
requestLayout()
}
- fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) {
- this.linkPreview = linkPreview
- val linkPreviewDraftView = this.linkPreviewDraftView ?: return
- linkPreviewDraftView.update(glide, linkPreview)
+ fun updateLinkPreviewDraft(glide: GlideRequests, updatedLinkPreview: LinkPreview) {
+ // Update our `linkPreview` property with the new (provided as an argument to this function)
+ // then update the View from that.
+ linkPreview = updatedLinkPreview.also { linkPreviewDraftView?.update(glide, it) }
}
override fun cancelLinkPreviewDraft() {
- if (quote != null) { return }
+ binding.inputBarAdditionalContentContainer.removeView(linkPreviewDraftView)
linkPreview = null
- binding.inputBarAdditionalContentContainer.removeAllViews()
+ linkPreviewDraftView = null
requestLayout()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt
index ec45b6ca82..6d7281dc47 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt
@@ -4,8 +4,6 @@ import android.animation.FloatEvaluator
import android.animation.IntEvaluator
import android.animation.ValueAnimator
import android.content.Context
-import android.os.Handler
-import android.os.Looper
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
@@ -14,6 +12,11 @@ import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
import org.thoughtcrime.securesms.util.DateUtils
@@ -25,10 +28,10 @@ import java.util.Date
class InputBarRecordingView : RelativeLayout {
private lateinit var binding: ViewInputBarRecordingBinding
private var startTimestamp = 0L
- private val snHandler = Handler(Looper.getMainLooper())
private var dotViewAnimation: ValueAnimator? = null
private var pulseAnimation: ValueAnimator? = null
var delegate: InputBarRecordingViewDelegate? = null
+ private var timerJob: Job? = null
val lockView: LinearLayout
get() = binding.lockView
@@ -50,9 +53,10 @@ class InputBarRecordingView : RelativeLayout {
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
binding.inputBarMiddleContentContainer.disableClipping()
binding.inputBarCancelButton.setOnClickListener { hide() }
+
}
- fun show() {
+ fun show(scope: CoroutineScope) {
startTimestamp = Date().time
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
binding.inputBarCancelButton.alpha = 0.0f
@@ -69,7 +73,7 @@ class InputBarRecordingView : RelativeLayout {
animateDotView()
pulse()
animateLockViewUp()
- updateTimer()
+ startTimer(scope)
}
fun hide() {
@@ -86,6 +90,24 @@ class InputBarRecordingView : RelativeLayout {
}
animation.start()
delegate?.handleVoiceMessageUIHidden()
+ stopTimer()
+ }
+
+ private fun startTimer(scope: CoroutineScope) {
+ timerJob?.cancel()
+ timerJob = scope.launch {
+ while (isActive) {
+ val duration = (Date().time - startTimestamp) / 1000L
+ binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
+
+ delay(500)
+ }
+ }
+ }
+
+ private fun stopTimer() {
+ timerJob?.cancel()
+ timerJob = null
}
private fun animateDotView() {
@@ -129,12 +151,6 @@ class InputBarRecordingView : RelativeLayout {
animation.start()
}
- private fun updateTimer() {
- val duration = (Date().time - startTimestamp) / 1000L
- binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
- snHandler.postDelayed({ updateTimer() }, 500)
- }
-
fun lock() {
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
fadeOutAnimation.duration = 250L
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
index a21ba1b502..2d8f745967 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
@@ -28,11 +28,10 @@ class MentionCandidateView : RelativeLayout {
private fun update() = with(binding) {
mentionCandidateNameTextView.text = candidate.displayName
- profilePictureView.root.publicKey = candidate.publicKey
- profilePictureView.root.displayName = candidate.displayName
- profilePictureView.root.additionalPublicKey = null
- profilePictureView.root.glide = glide!!
- profilePictureView.root.update()
+ profilePictureView.publicKey = candidate.publicKey
+ profilePictureView.displayName = candidate.displayName
+ profilePictureView.additionalPublicKey = null
+ profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt
index 401ccaa3c2..e62f7f8f85 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import dagger.hilt.android.AndroidEntryPoint
+import network.loki.messenger.R
import org.session.libsession.messaging.mentions.Mention
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.mms.GlideRequests
@@ -41,7 +42,9 @@ class MentionCandidatesView(context: Context, attrs: AttributeSet?, defStyleAttr
override fun getItem(position: Int): Mention { return candidates[position] }
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
- val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context)
+ val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context).apply {
+ contentDescription = context.getString(R.string.AccessibilityId_contact)
+ }
val mentionCandidate = getItem(position)
cell.glide = glide
cell.candidate = mentionCandidate
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
index cab24ce8be..21398c71aa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
@@ -65,17 +65,19 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
// Copy Session ID
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
- (thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.recipient.address.toString() != userPublicKey)
+ (thread.isGroupRecipient && !thread.isCommunityRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
// Message detail
- menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
+ menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
// Resend
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
+ // Resync
+ menu.findItem(R.id.menu_context_resync).isVisible = (selectedItems.size == 1 && firstMessage.isSyncFailed)
// Save media
menu.findItem(R.id.menu_context_save_attachment).isVisible = (selectedItems.size == 1
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
// Reply
menu.findItem(R.id.menu_context_reply).isVisible =
- (selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed)
+ (selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed && !firstMessage.isOpenGroupInvitation)
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean {
@@ -90,6 +92,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
R.id.menu_context_ban_and_delete_all -> delegate?.banAndDeleteAll(selectedItems)
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems)
+ R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
@@ -101,6 +104,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
override fun onDestroyActionMode(mode: ActionMode) {
adapter.selectedItems.clear()
adapter.notifyDataSetChanged()
+ delegate?.destroyActionMode()
}
}
@@ -112,8 +116,10 @@ interface ConversationActionModeCallbackDelegate {
fun banAndDeleteAll(messages: Set)
fun copyMessages(messages: Set)
fun copySessionID(messages: Set)
+ fun resyncMessage(messages: Set)
fun resendMessage(messages: Set)
fun showMessageDetail(messages: Set)
fun saveAttachment(messages: Set)
fun reply(messages: Set)
+ fun destroyActionMode()
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
index 663dd2e255..11069937a0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
@@ -4,17 +4,11 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
import android.os.AsyncTask
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
-import android.widget.ImageView
-import android.widget.TextView
import android.widget.Toast
-import androidx.annotation.ColorInt
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.SearchView
@@ -25,15 +19,12 @@ import androidx.core.graphics.drawable.IconCompat
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.leave
-import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.MediaOverviewActivity
-import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.ShortcutLauncherActivity
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
@@ -44,6 +35,8 @@ import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
import org.thoughtcrime.securesms.service.WebRtcCallService
+import org.thoughtcrime.securesms.showMuteDialog
+import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
@@ -53,36 +46,26 @@ object ConversationMenuHelper {
menu: Menu,
inflater: MenuInflater,
thread: Recipient,
- threadId: Long,
- context: Context,
- onOptionsItemSelected: (MenuItem) -> Unit
+ context: Context
) {
// Prepare
menu.clear()
- val isOpenGroup = thread.isOpenGroupRecipient
+ val isOpenGroup = thread.isCommunityRecipient
// Base menu (options that should always be present)
inflater.inflate(R.menu.menu_conversation, menu)
// Expiring messages
- if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient)) {
- if (thread.expireMessages > 0) {
- inflater.inflate(R.menu.menu_conversation_expiration_on, menu)
- val item = menu.findItem(R.id.menu_expiring_messages)
- val actionView = item.actionView
- val iconView = actionView.findViewById(R.id.menu_badge_icon)
- val badgeView = actionView.findViewById(R.id.expiration_badge)
- @ColorInt val color = context.getColorFromAttr(android.R.attr.textColorPrimary)
- iconView.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)
- badgeView.text = ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, thread.expireMessages)
- actionView.setOnClickListener { onOptionsItemSelected(item) }
- } else {
- inflater.inflate(R.menu.menu_conversation_expiration_off, menu)
- }
+ if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
+ inflater.inflate(R.menu.menu_conversation_expiration, menu)
+ }
+ // One-on-one chat menu allows copying the session id
+ if (thread.isContactRecipient) {
+ inflater.inflate(R.menu.menu_conversation_copy_session_id, menu)
}
// One-on-one chat menu (options that should only be present for one-on-one chats)
if (thread.isContactRecipient) {
if (thread.isBlocked) {
inflater.inflate(R.menu.menu_conversation_unblock, menu)
- } else {
+ } else if (!thread.isLocalNumber) {
inflater.inflate(R.menu.menu_conversation_block, menu)
}
}
@@ -105,7 +88,7 @@ object ConversationMenuHelper {
inflater.inflate(R.menu.menu_conversation_notification_settings, menu)
}
- if (!thread.isGroupRecipient && thread.hasApprovedMe()) {
+ if (thread.showCallMenu()) {
inflater.inflate(R.menu.menu_conversation_call, menu)
}
@@ -148,12 +131,12 @@ object ConversationMenuHelper {
R.id.menu_view_all_media -> { showAllMedia(context, thread) }
R.id.menu_search -> { search(context) }
R.id.menu_add_shortcut -> { addShortcut(context, thread) }
- R.id.menu_expiring_messages -> { showExpiringMessagesDialog(context, thread) }
- R.id.menu_expiring_messages_off -> { showExpiringMessagesDialog(context, thread) }
+ R.id.menu_expiring_messages -> { showDisappearingMessages(context, thread) }
R.id.menu_unblock -> { unblock(context, thread) }
R.id.menu_block -> { block(context, thread, deleteThread = false) }
R.id.menu_block_delete -> { blockAndDelete(context, thread) }
R.id.menu_copy_session_id -> { copySessionID(context, thread) }
+ R.id.menu_copy_open_group_url -> { copyOpenGroupUrl(context, thread) }
R.id.menu_edit_group -> { editClosedGroup(context, thread) }
R.id.menu_leave_group -> { leaveClosedGroup(context, thread) }
R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) }
@@ -180,26 +163,23 @@ object ConversationMenuHelper {
private fun call(context: Context, thread: Recipient) {
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
- AlertDialog.Builder(context)
- .setTitle(R.string.ConversationActivity_call_title)
- .setMessage(R.string.ConversationActivity_call_prompt)
- .setPositiveButton(R.string.activity_settings_title) { _, _ ->
- val intent = Intent(context, PrivacySettingsActivity::class.java)
- context.startActivity(intent)
+ context.showSessionDialog {
+ title(R.string.ConversationActivity_call_title)
+ text(R.string.ConversationActivity_call_prompt)
+ button(R.string.activity_settings_title, R.string.AccessibilityId_settings) {
+ Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
}
- .setNeutralButton(R.string.cancel) { d, _ ->
- d.dismiss()
- }.show()
+ cancelButton()
+ }
return
}
- val service = WebRtcCallService.createCall(context, thread)
- context.startService(service)
+ WebRtcCallService.createCall(context, thread)
+ .let(context::startService)
- val activity = Intent(context, WebRtcCallActivity::class.java).apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- }
- context.startActivity(activity)
+ Intent(context, WebRtcCallActivity::class.java)
+ .apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
+ .let(context::startActivity)
}
@@ -207,6 +187,7 @@ object ConversationMenuHelper {
private fun addShortcut(context: Context, thread: Recipient) {
object : AsyncTask() {
+ @Deprecated("Deprecated in Java")
override fun doInBackground(vararg params: Void?): IconCompat? {
var icon: IconCompat? = null
val contactPhoto = thread.contactPhoto
@@ -225,6 +206,7 @@ object ConversationMenuHelper {
return icon
}
+ @Deprecated("Deprecated in Java")
override fun onPostExecute(icon: IconCompat?) {
val name = Optional.fromNullable(thread.name)
.or(Optional.fromNullable(thread.profileName))
@@ -241,9 +223,9 @@ object ConversationMenuHelper {
}.execute()
}
- private fun showExpiringMessagesDialog(context: Context, thread: Recipient) {
+ private fun showDisappearingMessages(context: Context, thread: Recipient) {
val listener = context as? ConversationMenuListener ?: return
- listener.showExpiringMessagesDialog(thread)
+ listener.showDisappearingMessages(thread)
}
private fun unblock(context: Context, thread: Recipient) {
@@ -270,6 +252,12 @@ object ConversationMenuHelper {
listener.copySessionID(thread.address.toString())
}
+ private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
+ if (!thread.isCommunityRecipient) { return }
+ val listener = context as? ConversationMenuListener ?: return
+ listener.copyOpenGroupUrl(thread)
+ }
+
private fun editClosedGroup(context: Context, thread: Recipient) {
if (!thread.isClosedGroupRecipient) { return }
val intent = Intent(context, EditClosedGroupActivity::class.java)
@@ -280,9 +268,7 @@ object ConversationMenuHelper {
private fun leaveClosedGroup(context: Context, thread: Recipient) {
if (!thread.isClosedGroupRecipient) { return }
- val builder = AlertDialog.Builder(context)
- builder.setTitle(context.resources.getString(R.string.ConversationActivity_leave_group))
- builder.setCancelable(true)
+
val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
val admins = group.admins
val sessionID = TextSecurePreferences.getLocalNumber(context)
@@ -292,33 +278,29 @@ object ConversationMenuHelper {
} else {
context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)
}
- builder.setMessage(message)
- builder.setPositiveButton(R.string.yes) { _, _ ->
- var groupPublicKey: String?
- var isClosedGroup: Boolean
- try {
- groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
- isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
- } catch (e: IOException) {
- groupPublicKey = null
- isClosedGroup = false
- }
- try {
- if (isClosedGroup) {
- MessageSender.leave(groupPublicKey!!, true)
- } else {
- Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
+
+ fun onLeaveFailed() = Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
+
+ context.showSessionDialog {
+ title(R.string.ConversationActivity_leave_group)
+ text(message)
+ button(R.string.yes) {
+ try {
+ val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
+ val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
+
+ if (isClosedGroup) MessageSender.leave(groupPublicKey, notifyUser = false)
+ else onLeaveFailed()
+ } catch (e: Exception) {
+ onLeaveFailed()
}
- } catch (e: Exception) {
- Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
}
+ button(R.string.no)
}
- builder.setNegativeButton(R.string.no, null)
- builder.show()
}
private fun inviteContacts(context: Context, thread: Recipient) {
- if (!thread.isOpenGroupRecipient) { return }
+ if (!thread.isCommunityRecipient) { return }
val intent = Intent(context, SelectContactsActivity::class.java)
val activity = context as AppCompatActivity
activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS)
@@ -329,7 +311,7 @@ object ConversationMenuHelper {
}
private fun mute(context: Context, thread: Recipient) {
- MuteDialog.show(ContextThemeWrapper(context, context.theme)) { until: Long ->
+ showMuteDialog(ContextThemeWrapper(context, context.theme)) { until ->
DatabaseComponent.get(context).recipientDatabase().setMuted(thread, until)
}
}
@@ -344,7 +326,8 @@ object ConversationMenuHelper {
fun block(deleteThread: Boolean = false)
fun unblock()
fun copySessionID(sessionId: String)
- fun showExpiringMessagesDialog(thread: Recipient)
+ fun copyOpenGroupUrl(thread: Recipient)
+ fun showDisappearingMessages(thread: Recipient)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt
index a4e4a52d5b..1177b4afc9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt
@@ -3,49 +3,80 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
-import android.view.View
import android.widget.LinearLayout
import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
+import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewControlMessageBinding
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.messaging.messages.ExpirationConfiguration
+import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages
+import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode
import org.thoughtcrime.securesms.database.model.MessageRecord
+import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import javax.inject.Inject
+@AndroidEntryPoint
class ControlMessageView : LinearLayout {
- private lateinit var binding: ViewControlMessageBinding
+ private val TAG = "ControlMessageView"
- // region Lifecycle
- constructor(context: Context) : super(context) { initialize() }
- constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
- constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
+ private val binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
- private fun initialize() {
- binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ @Inject lateinit var disappearingMessages: DisappearingMessages
+
+ init {
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
}
- // endregion
- // region Updating
fun bind(message: MessageRecord, previous: MessageRecord?) {
binding.dateBreakTextView.showDateBreak(message, previous)
- binding.iconImageView.visibility = View.GONE
+ binding.iconImageView.isGone = true
+ binding.expirationTimerView.isGone = true
+ binding.followSetting.isGone = true
var messageBody: CharSequence = message.getDisplayBody(context)
+ binding.root.contentDescription = null
+ binding.textView.text = messageBody
when {
message.isExpirationTimerUpdate -> {
- binding.iconImageView.setImageDrawable(
- ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)
- )
- binding.iconImageView.visibility = View.VISIBLE
+ binding.apply {
+ expirationTimerView.isVisible = true
+
+ val threadRecipient = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(message.threadId)
+
+ if (threadRecipient?.isClosedGroupRecipient == true) {
+ expirationTimerView.setTimerIcon()
+ } else {
+ expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
+ }
+
+ followSetting.isVisible = ExpirationConfiguration.isNewConfigEnabled
+ && !message.isOutgoing
+ && message.expiryMode != (MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId)?.expiryMode ?: ExpiryMode.NONE)
+ && threadRecipient?.isGroupRecipient != true
+
+ followSetting.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
+ }
}
message.isMediaSavedNotification -> {
- binding.iconImageView.setImageDrawable(
- ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)
- )
- binding.iconImageView.visibility = View.VISIBLE
+ binding.iconImageView.apply {
+ setImageDrawable(
+ ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)
+ )
+ isVisible = true
+ }
}
message.isMessageRequestResponse -> {
- messageBody = context.getString(R.string.message_requests_accepted)
+ binding.textView.text = context.getString(R.string.message_requests_accepted)
+ binding.root.contentDescription=context.getString(R.string.AccessibilityId_message_request_config_message)
}
message.isCallLog -> {
val drawable = when {
@@ -54,16 +85,22 @@ class ControlMessageView : LinearLayout {
message.isFirstMissedCall -> R.drawable.ic_info_outline_light
else -> R.drawable.ic_missed_call
}
- binding.iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, drawable, context.theme))
- binding.iconImageView.visibility = View.VISIBLE
+ binding.textView.isVisible = false
+ binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(ResourcesCompat.getDrawable(resources, drawable, context.theme), null, null, null)
+ binding.callTextView.text = messageBody
+
+ if (message.expireStarted > 0 && message.expiresIn > 0) {
+ binding.expirationTimerView.isVisible = true
+ binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
+ }
}
}
- binding.textView.text = messageBody
+ binding.textView.isGone = message.isCallLog
+ binding.callView.isVisible = message.isCallLog
}
fun recycle() {
}
- // endregion
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt
index f4f1a2cd97..0614b52e84 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt
@@ -5,11 +5,13 @@ import android.content.res.ColorStateList
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.annotation.ColorInt
+import androidx.core.view.isVisible
import network.loki.messenger.databinding.ViewDocumentBinding
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
class DocumentView : LinearLayout {
private val binding: ViewDocumentBinding by lazy { ViewDocumentBinding.bind(this) }
+
// region Lifecycle
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
@@ -22,6 +24,12 @@ class DocumentView : LinearLayout {
binding.documentTitleTextView.text = document.fileName.or("Untitled File")
binding.documentTitleTextView.setTextColor(textColor)
binding.documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
+
+ // Show the progress spinner if the attachment is downloading, otherwise show
+ // the document icon (and always remove the other, whichever one that is)
+ binding.documentViewProgress.isVisible = message.isMediaPending
+ binding.documentViewIconImageView.isVisible = !message.isMediaPending
}
// endregion
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java
deleted file mode 100644
index 6d16f1f421..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java
+++ /dev/null
@@ -1,346 +0,0 @@
-package org.thoughtcrime.securesms.conversation.v2.messages;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.AttributeSet;
-import android.view.HapticFeedbackConstants;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.constraintlayout.widget.Group;
-import androidx.core.content.ContextCompat;
-
-import com.google.android.flexbox.FlexboxLayout;
-import com.google.android.flexbox.JustifyContent;
-
-import org.session.libsession.utilities.TextSecurePreferences;
-import org.session.libsession.utilities.ThemeUtil;
-import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
-import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
-import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
-import org.thoughtcrime.securesms.database.model.MessageId;
-import org.thoughtcrime.securesms.database.model.ReactionRecord;
-import org.thoughtcrime.securesms.util.NumberUtil;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import network.loki.messenger.R;
-
-public class EmojiReactionsView extends LinearLayout implements View.OnTouchListener {
-
- // Normally 6dp, but we have 1dp left+right margin on the pills themselves
- private final int OUTER_MARGIN = ViewUtil.dpToPx(2);
- private static final int DEFAULT_THRESHOLD = 5;
-
- private List