diff --git a/.gitignore b/.gitignore index 01ec4c41c..1fe35a0e7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ signing.properties ffpr *.sh pkcs11.password -play +app/play diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..b650b98b1 --- /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/BUILDING.md b/BUILDING.md index e78207d96..48b4412dd 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 341fd4284..723d50c75 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ 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 3dcc19726..61f560189 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,4 @@ + buildscript { repositories { google() @@ -13,12 +14,16 @@ buildscript { } } +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,141 +31,8 @@ configurations.all { exclude module: "commons-logging" } -dependencies { - 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:$preferenceVersion" - implementation 'androidx.legacy:legacy-preference-v14:1.0.0' - implementation 'androidx.gridlayout:gridlayout:1.0.0' - 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.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" - implementation ("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' - } - 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' - implementation 'org.signal:aesgcmprovider:0.0.3' - implementation 'org.webrtc:google-webrtc:1.0.32006' - implementation "me.leolin:ShortcutBadger:1.1.16" - implementation 'se.emilsjolander:stickylistheaders:2.7.0' - implementation 'com.jpardogo.materialtabstrip:library:1.0.9' - implementation 'org.apache.httpcomponents:httpclient-android:4.3.5' - implementation 'commons-net:commons-net:3.7.2' - implementation 'com.github.chrisbanes:PhotoView:2.1.3' - implementation "com.github.bumptech.glide:glide:$glideVersion" - annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" - kapt "com.github.bumptech.glide:compiler:$glideVersion" - implementation 'com.makeramen:roundedimageview:2.1.0' - implementation 'com.pnikosis:materialish-progress:1.5' - implementation 'org.greenrobot:eventbus:3.0.0' - implementation 'pl.tajchert:waitingdots:0.1.0' - implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' - implementation 'com.melnykov:floatingactionbutton:1.3.0' - implementation 'com.google.zxing:android-integration:3.1.0' - implementation "com.google.dagger:hilt-android:$daggerVersion" - kapt "com.google.dagger:hilt-compiler:$daggerVersion" - implementation 'mobi.upod:time-duration-picker:1.1.3' - implementation 'com.google.zxing:core:3.2.1' - implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { - exclude group: 'com.android.support', module: 'support-annotations' - } - implementation ('cn.carbswang.android:NumberPickerView:1.0.9') { - exclude group: 'com.android.support', module: 'appcompat-v7' - } - implementation ('com.tomergoldst.android:tooltips:1.0.6') { - exclude group: 'com.android.support', module: 'appcompat-v7' - } - implementation ('com.klinkerapps:android-smsmms:4.0.1') { - exclude group: 'com.squareup.okhttp', module: 'okhttp' - exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection' - } - implementation '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 'androidx.sqlite:sqlite-ktx:2.2.0' - implementation 'net.zetetic:sqlcipher-android:4.5.3@aar' - implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') { - exclude group: 'com.fasterxml.jackson.core' - exclude group: 'org.freemarker' - } - implementation project(":libsignal") - implementation project(":libsession") - 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 "com.google.protobuf:protobuf-java:$protobufVersion" - implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" - implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" - implementation 'app.cash.copper:copper-flow:1.0.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - 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:$junitVersion" - testImplementation 'org.assertj:assertj-core:3.11.1' - testImplementation "org.mockito:mockito-inline:4.0.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:$testCoreVersion" - testImplementation "androidx.arch.core:core-testing:2.1.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' - - // AndroidJUnitRunner and JUnit Rules - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - - // Assertions - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.ext:truth:1.4.0' - 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.1' - - testImplementation 'org.robolectric:robolectric:4.4' - testImplementation 'org.robolectric:shadows-multidex:4.4' -} - -def canonicalVersionCode = 323 -def canonicalVersionName = "1.16.3" +def canonicalVersionCode = 354 +def canonicalVersionName = "1.17.0" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, @@ -202,6 +74,13 @@ android { } } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.4.7' + } + defaultConfig { versionCode canonicalVersionCode * postFixSize versionName canonicalVersionName @@ -250,14 +129,28 @@ android { flavorDimensions "distribution" productFlavors { play { + 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" } + 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" + + } + 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\"" } } @@ -288,6 +181,166 @@ android { 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) + } + } + } + } + } +} + +dependencies { + + 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:$preferenceVersion" + implementation 'androidx.legacy:legacy-preference-v14:1.0.0' + implementation 'androidx.gridlayout:gridlayout:1.0.0' + 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-runtime-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" + 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' + implementation 'org.signal:aesgcmprovider:0.0.3' + implementation 'org.webrtc:google-webrtc:1.0.32006' + implementation "me.leolin:ShortcutBadger:1.1.16" + implementation 'se.emilsjolander:stickylistheaders:2.7.0' + implementation 'com.jpardogo.materialtabstrip:library:1.0.9' + implementation 'org.apache.httpcomponents:httpclient-android:4.3.5' + implementation 'commons-net:commons-net:3.7.2' + implementation 'com.github.chrisbanes:PhotoView:2.1.3' + implementation "com.github.bumptech.glide:glide:$glideVersion" + annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" + kapt "com.github.bumptech.glide:compiler:$glideVersion" + implementation 'com.makeramen:roundedimageview:2.1.0' + implementation 'com.pnikosis:materialish-progress:1.5' + implementation 'org.greenrobot:eventbus:3.0.0' + implementation 'pl.tajchert:waitingdots:0.1.0' + implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' + implementation 'com.melnykov:floatingactionbutton:1.3.0' + implementation 'com.google.zxing:android-integration:3.1.0' + implementation "com.google.dagger:hilt-android:$daggerVersion" + kapt "com.google.dagger:hilt-compiler:$daggerVersion" + implementation 'mobi.upod:time-duration-picker:1.1.3' + implementation 'com.google.zxing:core:3.2.1' + implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { + exclude group: 'com.android.support', module: 'support-annotations' + } + implementation ('cn.carbswang.android:NumberPickerView:1.0.9') { + exclude group: 'com.android.support', module: 'appcompat-v7' + } + implementation ('com.tomergoldst.android:tooltips:1.0.6') { + exclude group: 'com.android.support', module: 'appcompat-v7' + } + implementation ('com.klinkerapps:android-smsmms:4.0.1') { + exclude group: 'com.squareup.okhttp', module: 'okhttp' + exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection' + } + implementation 'com.annimon:stream:1.1.8' + implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4' + implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2' + 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 "com.google.protobuf:protobuf-java:$protobufVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + implementation 'app.cash.copper:copper-flow:1.0.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" + implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" + implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion" + 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:$junitVersion" + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation "org.mockito:mockito-inline:4.10.0" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + androidTestImplementation "org.mockito:mockito-android:4.10.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:$testCoreVersion" + + androidTestImplementation('com.adevinta.android:barista:4.2.0') { + exclude group: 'org.jetbrains.kotlin' + } + + // AndroidJUnitRunner and JUnit Rules + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test:rules:1.5.0' + + // Assertions + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.ext:truth:1.5.0' + androidTestImplementation 'com.google.truth:truth:1.1.3' + + // Espresso dependencies + 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' + + implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1' + implementation 'androidx.compose.ui:ui:1.4.3' + implementation 'androidx.compose.ui:ui-tooling:1.4.3' + implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.31.5-beta" + implementation "com.google.accompanist:accompanist-pager-indicators:0.31.5-beta" + implementation "androidx.compose.runtime:runtime-livedata:1.4.3" + + implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02' + implementation 'androidx.compose.material:material:1.5.0-alpha02' } static def getLastCommitTimestamp() { @@ -308,3 +361,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 087d48689..eabe06f7d 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()) } 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 000000000..59cb8ede0 --- /dev/null +++ b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt @@ -0,0 +1,102 @@ +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.util.Contact +import network.loki.messenger.libsession_util.util.ExpiryMode +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argThat +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.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.Companion.newInstance(key) + contactList.forEach { contact -> + contacts.set(contact) + } + return contacts.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 + }) + verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true)) + } + +} \ 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 000000000..dad7ab3ac --- /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 000000000..0c81d0477 --- /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 000000000..26a484df1 --- /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 000000000..dc7bf893d --- /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 000000000..9d9b61ce9 --- /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 000000000..78d42b3e3 --- /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 5cdaec7a8..331cd53e0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,12 +29,16 @@ android:name="android.hardware.touchscreen" android:required="false" /> + + + + @@ -313,14 +317,6 @@ android:name="android.support.PARENT_ACTIVITY" android:value="org.thoughtcrime.securesms.home.HomeActivity" /> - - - - - - - - - - @@ -453,17 +443,9 @@ - - diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index e506967ff..1102c68e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -41,6 +41,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; @@ -56,35 +58,29 @@ 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.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.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.jobs.FastJobStorage; -import org.thoughtcrime.securesms.jobs.JobManagerFactories; 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; @@ -114,7 +110,8 @@ import dagger.hilt.EntryPoints; 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. @@ -125,7 +122,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"; @@ -134,7 +131,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; @@ -147,10 +143,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO private PersistentLogger persistentLogger; @Inject LokiAPIDatabase lokiAPIDatabase; - @Inject StorageProtocol storage; + @Inject public Storage storage; + @Inject Device device; @Inject MessageDataProvider messageDataProvider; - @Inject JobDatabase jobDatabase; @Inject TextSecurePreferences textSecurePreferences; + @Inject PushRegistry pushRegistry; + @Inject ConfigFactory configFactory; CallMessageProcessor callMessageProcessor; MessagingModuleConfiguration messagingModuleConfiguration; @@ -169,7 +167,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO } public TextSecurePreferences getPrefs() { - return textSecurePreferences; + return EntryPoints.get(getApplicationContext(), AppComponent.class).getPrefs(); } public DatabaseComponent getDatabaseComponent() { @@ -198,18 +196,28 @@ public class ApplicationContext extends Application implements DefaultLifecycleO return this.persistentLogger; } + @Override + public void notifyUpdates(@NonNull ConfigBase forConfigObject) { + // forward to the config factory / storage ig + if (forConfigObject instanceof UserProfile && !textSecurePreferences.getConfigurationMessageSynced()) { + textSecurePreferences.setConfigurationMessageSynced(true); + } + storage.notifyConfigUpdates(forConfigObject); + } + @Override public void onCreate() { 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 + ); callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage); Log.i(TAG, "onCreate()"); startKovenant(); @@ -223,10 +231,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(); @@ -234,7 +238,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO initializeProfileManager(); initializePeriodicTasks(); SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager()); - initializeJobManager(); initializeWebRtc(); initializeBlobProvider(); resubmitProfilePictureIfNeeded(); @@ -277,7 +280,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO if (poller != null) { poller.stopIfNeeded(); } - ClosedGroupPollerV2.getShared().stop(); + ClosedGroupPollerV2.getShared().stopAll(); } @Override @@ -291,10 +294,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO LocaleParser.Companion.configure(new LocaleParseHelper()); } - public JobManager getJobManager() { - return jobManager; - } - public ExpiringMessageManager getExpiringMessageManager() { return expiringMessageManager; } @@ -357,16 +356,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); } @@ -380,7 +369,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO } private void initializeProfileManager() { - this.profileManager = new ProfileManager(); + this.profileManager = new ProfileManager(this, configFactory); } private void initializeTypingStatusSender() { @@ -389,10 +378,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() { @@ -443,29 +428,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; @@ -473,7 +435,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO poller.setUserPublicKey(userPublicKey); return; } - poller = new Poller(); + poller = new Poller(configFactory, new Timer()); } public void startPollingIfNeeded() { @@ -516,6 +478,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO }); } catch (Exception exception) { // Do nothing + Log.e("Loki-Avatar", "Uploading avatar failed", exception); } }); } @@ -535,24 +498,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(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 7d82c760c..51f66ec32 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; @@ -92,6 +94,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 93313e527..000000000 --- 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 000000000..af38c31ff --- /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 000000000..0390a3007 --- /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 000000000..bdfa9b608 --- /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 469629ed3..000000000 --- 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/ExpirationDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.kt new file mode 100644 index 000000000..9a34c1ec4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.kt @@ -0,0 +1,51 @@ +package org.thoughtcrime.securesms + +import android.content.Context +import android.view.LayoutInflater +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import cn.carbswang.android.numberpickerview.library.NumberPickerView +import network.loki.messenger.R +import org.session.libsession.utilities.ExpirationUtil + +fun Context.showExpirationDialog( + expiration: Int, + onExpirationTime: (Int) -> Unit +): AlertDialog { + val view = LayoutInflater.from(this).inflate(R.layout.expiration_dialog, null) + val numberPickerView = view.findViewById(R.id.expiration_number_picker) + + fun updateText(index: Int) { + view.findViewById(R.id.expiration_details).text = when (index) { + 0 -> getString(R.string.ExpirationDialog_your_messages_will_not_expire) + else -> getString( + R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, + numberPickerView.displayedValues[index] + ) + } + } + + val expirationTimes = resources.getIntArray(R.array.expiration_times) + val expirationDisplayValues = expirationTimes + .map { ExpirationUtil.getExpirationDisplayValue(this, it) } + .toTypedArray() + + val selectedIndex = expirationTimes.run { indexOfFirst { it >= expiration }.coerceIn(indices) } + + numberPickerView.apply { + displayedValues = expirationDisplayValues + minValue = 0 + maxValue = expirationTimes.lastIndex + setOnValueChangedListener { _, _, index -> updateText(index) } + value = selectedIndex + } + + updateText(selectedIndex) + + return showSessionDialog { + title(getString(R.string.ExpirationDialog_disappearing_messages)) + view(view) + okButton { onExpirationTime(numberPickerView.let { expirationTimes[it.value] }) } + cancelButton() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java index 63b287433..49527c238 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java @@ -57,6 +57,7 @@ import org.session.libsession.database.StorageProtocol; import org.session.libsession.messaging.MessagingModuleConfiguration; 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.session.libsession.utilities.GroupRecord; import org.session.libsession.utilities.TextSecurePreferences; @@ -356,9 +357,9 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i @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) @@ -400,34 +401,25 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i }.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(), + DeleteMediaDialog.show( + requireContext(), + recordCount, + () -> + new ProgressDialogAsyncTask(requireContext(), R.string.MediaOverviewActivity_Media_delete_progress_title, R.string.MediaOverviewActivity_Media_delete_progress_message) { @Override @@ -442,11 +434,8 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i return null; } - }.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()])); - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } + }.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()]))); + } private void handleSelectAllMedia() { getListAdapter().selectAllMedia(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index b21c7dac8..f19a1fc45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -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; /** @@ -145,6 +147,10 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } }; + 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; if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { @@ -415,7 +421,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 +429,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 +438,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 +455,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 @@ -530,7 +528,6 @@ 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); 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 000000000..00e2c3d6d --- /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 ca6cf8f6c..000000000 --- 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 acca9f837..000000000 --- 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 000000000..f294e387f --- /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 63b42c493..afc993df8 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/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt new file mode 100644 index 000000000..44c30741e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt @@ -0,0 +1,146 @@ +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.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.setPadding +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 dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context) + + private var dialog: AlertDialog? = null + private fun dismiss() = dialog?.dismiss() + + private val topView = LinearLayout(context).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) } + } + + 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, dp20) } + } + } + + + 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) + } + + 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, + 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, WRAP_CONTENT, 1f) + .apply { setMargins(toPx(20, resources)) } + 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 3dd5cd8cc..000000000 --- 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/ScreenshotObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt index 84a9b6cfc..9c7ca21e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt @@ -7,6 +7,10 @@ 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) { @@ -31,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) } } @@ -56,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/backup/BackupEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupEvent.kt deleted file mode 100644 index 614dc30bb..000000000 --- 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 eec2a2e58..000000000 --- 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 8ddfc23a8..000000000 --- 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 f3b78606f..000000000 --- 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 - 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 - 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 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 - 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/FullBackupExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt deleted file mode 100644 index 6b5d47a2e..000000000 --- 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.zetetic.database.sqlcipher.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 b40c049bc..000000000 --- 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.zetetic.database.sqlcipher.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 7e732d1aa..b87eac12c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt @@ -249,17 +249,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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index 195c066d4..1ac4f8442 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -13,6 +13,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.session.libsession.snode.SnodeAPI; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -106,7 +107,7 @@ public class ConversationItemFooter extends LinearLayout { messageRecord.getExpiresIn()); this.timerView.startAnimation(); - if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= System.currentTimeMillis()) { + if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= SnodeAPI.getNowWithOffset()) { ApplicationContext.getInstance(getContext()).getExpiringMessageManager().checkSchedule(); } } else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) { 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 61094fb7d..157bc215e 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 d17790012..406c878ec 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 df36719db..000000000 --- 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/Outliner.java b/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java deleted file mode 100644 index cb6cfc7ab..000000000 --- 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 a827a7d26..604422460 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -2,6 +2,7 @@ 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 @@ -9,6 +10,7 @@ import androidx.annotation.DimenRes import com.bumptech.glide.load.engine.DiskCacheStrategy import network.loki.messenger.R import network.loki.messenger.databinding.ViewProfilePictureBinding +import network.loki.messenger.databinding.ViewUserBinding import org.session.libsession.avatars.ContactColors import org.session.libsession.avatars.PlaceholderAvatarPhoto import org.session.libsession.avatars.ProfileContactPhoto @@ -18,13 +20,14 @@ 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 @@ -32,13 +35,18 @@ class ProfilePictureView @JvmOverloads constructor( 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 unknownOpenGroupDrawable = ResourceContactPhoto(R.drawable.ic_notification) - .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) + 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 { @@ -52,12 +60,19 @@ class ProfilePictureView @JvmOverloads constructor( .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()) this.publicKey = publicKey @@ -73,7 +88,6 @@ class ProfilePictureView @JvmOverloads constructor( } fun update() { - if (!this::glide.isInitialized) return val publicKey = publicKey ?: return val additionalPublicKey = additionalPublicKey if (additionalPublicKey != null) { @@ -108,30 +122,36 @@ class ProfilePictureView @JvmOverloads constructor( val signalProfilePicture = recipient.contactPhoto val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject + val placeholder = PlaceholderAvatarPhoto(context, 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 if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) { glide.clear(imageView) - imageView.setImageDrawable(unknownOpenGroupDrawable) + glide.load(unknownOpenGroupDrawable) + .centerCrop() + .circleCrop() + .into(imageView) } else { - val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}") - glide.clear(imageView) 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 000000000..674847873 --- /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/emoji/parsing/EmojiPageBitmap.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java deleted file mode 100644 index 3c3a4fa3e..000000000 --- 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 358a9d326..d0b101a9f 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 @@ -5,8 +5,9 @@ import androidx.annotation.AttrRes /** * Represents an action to be rendered */ -data class ActionItem( +data class ActionItem @JvmOverloads constructor( @AttrRes val iconRes: Int, val title: CharSequence, - val action: Runnable + val action: Runnable, + val contentDescription: String? = 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 65fb1ddbb..c86b40dfa 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 @@ -77,6 +77,7 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) { context.theme.resolveAttribute(model.item.iconRes, typedValue, true) icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId)) } + itemView.contentDescription = model.item.contentDescription title.text = model.item.title itemView.setOnClickListener { model.item.action.run() 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 a1b45ac2a..000000000 --- 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/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 4a1059ffd..5284fb001 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/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt index e88cf1d08..36a8c1adf 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 ef783da79..000000000 --- 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/start/ContactListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt index 99e7c9061..68e2f975c 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,14 +32,13 @@ 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) } } fun unbind() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() } } 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 2e62932ab..92f050f76 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 c51ecedd4..7f1947c91 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 @@ -3,29 +3,50 @@ package org.thoughtcrime.securesms.conversation.v2 import android.Manifest import android.animation.FloatEvaluator import android.animation.ValueAnimator -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent import android.content.res.Resources import android.database.Cursor import android.graphics.Rect import android.graphics.Typeface import android.net.Uri -import android.os.* +import android.os.AsyncTask +import android.os.Build +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.* +import android.view.ActionMode +import android.view.Menu +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.isVisible +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.recyclerview.widget.LinearLayoutManager @@ -33,7 +54,13 @@ 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.consumeAsFlow +import kotlinx.coroutines.flow.filter 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 @@ -58,8 +85,13 @@ 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.utilities.* +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 +import org.session.libsession.utilities.MediaTypes +import org.session.libsession.utilities.Stub +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.RecipientModifiedListener @@ -70,16 +102,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.contacts.SelectContactsActivity.Companion.selectedContactsKey -import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher +import org.thoughtcrime.securesms.util.SimpleTextWatcher import org.thoughtcrime.securesms.conversation.settings.ConversationSettingsActivityContract import org.thoughtcrime.securesms.conversation.settings.ConversationSettingsActivityResult 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_REPLY +import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND +import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog @@ -94,7 +129,10 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel -import org.thoughtcrime.securesms.conversation.v2.utilities.* +import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager +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 @@ -107,6 +145,7 @@ 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 import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord @@ -120,13 +159,31 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivity -import org.thoughtcrime.securesms.mms.* +import org.thoughtcrime.securesms.mms.AudioSlide +import org.thoughtcrime.securesms.mms.GifSlide +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.mms.ImageSlide +import org.thoughtcrime.securesms.mms.MediaConstraints +import org.thoughtcrime.securesms.mms.Slide +import org.thoughtcrime.securesms.mms.SlideDeck +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.util.* -import java.util.* +import org.thoughtcrime.securesms.showExpirationDialog +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.isScrolledToBottom +import org.thoughtcrime.securesms.util.push +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 @@ -202,11 +259,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe it } val recipient = Recipient.from(this, address, false) - threadId = threadDb.getOrCreateThreadIdFor(recipient) + threadId = storage.getOrCreateThreadIdFor(recipient.address) } } ?: finish() } - viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair()) + viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair(), contentResolver) } private var actionMode: ActionMode? = null private var unreadCount = 0 @@ -227,11 +284,11 @@ 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 layoutManager: LinearLayoutManager? get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? } @@ -247,11 +304,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe 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) }, @@ -293,6 +356,7 @@ 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 @@ -323,12 +387,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // 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.isOpenGroupRecipient && openGroup == null)) { Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() return finish() } - setUpRecyclerView() + setUpToolBar() setUpInputBar() setUpLinkPreviewObserver() @@ -336,41 +401,64 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe setUpUiStateObserver() binding!!.scrollToBottomButton.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 { // 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 = layoutManager.findFirstVisibleItemPosition() - if (position > 10) { - binding?.conversationRecyclerView?.scrollToPosition(10) - } +// 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(0) + binding?.conversationRecyclerView?.smoothScrollToPosition(targetPosition) } } } - unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId) + updateUnreadCountIndicator() - setUpTypingObserver() - setUpRecipientObserver() updateSubtitle() - getLatestOpenGroupInfoIfNeeded() + updatePlaceholder() setUpBlockedBanner() binding!!.searchBottomBar.setEventListener(this) - setUpSearchResultObserver() - scrollToFirstUnreadMessageIfNeeded() + updateSendAfterApprovalText() showOrHideInputIfNeeded() 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() + + 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() + + if (author != null && messageTimestamp >= 0 && targetPosition >= 0) { + binding?.conversationRecyclerView?.scrollToPosition(targetPosition) + } + else { + scrollToFirstUnreadMessageIfNeeded(true) + } } } @@ -378,16 +466,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe ViewUtil.findStubById(this, R.id.conversation_reaction_scrubber_stub) reactionDelegate = ConversationReactionDelegate(reactionOverlayStub) reactionDelegate.setOnReactionSelectedListener(this) + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + // only update the conversation every 3 seconds maximum + // channel is rendezvous and shouldn't block on try send calls as often as we want + val bufferedFlow = bufferedLastSeenChannel.consumeAsFlow() + bufferedFlow.filter { + it > storage.getLastSeen(viewModel.threadId) + }.collectLatest { latestMessageRead -> + withContext(Dispatchers.IO) { + storage.markConversationAsRead(viewModel.threadId, latestMessageRead) + } + } + } + } } override fun onResume() { super.onResume() ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId) - val recipient = viewModel.recipient ?: return - - lifecycleScope.launch(Dispatchers.IO) { - threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient) - } contentResolver.registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, @@ -414,23 +511,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe push(intent, 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) + 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, null) + jumpToMessage(author, messageTimestamp, firstLoad.get(), null) + } + else if (firstLoad.getAndSet(false)) { + scrollToFirstUnreadMessageIfNeeded(true) + handleRecyclerViewScrolled() + } + else if (oldCount != newCount) { + handleRecyclerViewScrolled() } } + updatePlaceholder() } override fun onLoaderReset(cursor: Loader) { @@ -440,7 +559,7 @@ 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) @@ -449,18 +568,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { handleRecyclerViewScrolled() } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + + } }) + + binding!!.conversationRecyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + showScrollToBottomButtonIfApplicable() + } } // 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 { + binding.toolbarContent.conversationTitleView.text = when { recipient.isLocalNumber -> getString(R.string.note_to_self) else -> recipient.toShortString() } @@ -470,12 +598,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe 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 + binding.toolbarContent.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size) MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this) - val profilePictureView = binding!!.toolbarContent.profilePictureView.root - profilePictureView.update(recipient) - profilePictureView.setOnClickListener(this) + val profilePictureView = binding.toolbarContent.profilePictureView + viewModel.recipient?.let(profilePictureView::update) } // called from onCreate @@ -613,15 +739,37 @@ 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 { @@ -649,6 +797,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // region Animation & Updating override fun onModified(recipient: Recipient) { + viewModel.updateRecipient() + runOnUiThread { val threadRecipient = viewModel.recipient ?: return@runOnUiThread if (threadRecipient.isContactRecipient) { @@ -657,8 +807,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe setUpMessageRequestsBar() invalidateOptionsMenu() updateSubtitle() + updateSendAfterApprovalText() showOrHideInputIfNeeded() - binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient) + + binding?.toolbarContent?.profilePictureView?.update(threadRecipient) binding?.toolbarContent?.conversationTitleView?.text = when { threadRecipient.isLocalNumber -> getString(R.string.note_to_self) else -> threadRecipient.toShortString() @@ -666,6 +818,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } + private fun updateSendAfterApprovalText() { + binding?.textSendAfterApproval?.isVisible = viewModel.showSendAfterApprovalText + } + private fun showOrHideInputIfNeeded() { val recipient = viewModel.recipient if (recipient != null && recipient.isClosedGroupRecipient) { @@ -697,11 +853,8 @@ 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) } @@ -785,7 +938,9 @@ 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) @@ -897,20 +1052,62 @@ 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 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) { + val visibleItemTimestamp = adapter.getTimestampForItemAt(targetVisiblePosition) + if (visibleItemTimestamp != null) { + bufferedLastSeenChannel.trySend(visibleItemTimestamp) + } + } + + 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 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() + 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() { @@ -967,19 +1164,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) { @@ -989,33 +1185,44 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() } + override fun copyOpenGroupUrl(thread: Recipient) { + if (!thread.isOpenGroupRecipient) { 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 showExpiringMessagesDialog(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) + showExpirationDialog(thread.expireMessages) { expirationTime -> + storage.setExpirationTimer(thread.address.serialize(), expirationTime) val message = ExpirationTimerUpdate(expirationTime) message.recipient = thread.address.serialize() - message.sentTimestamp = System.currentTimeMillis() - val expiringMessageManager = ApplicationContext.getInstance(this).expiringMessageManager - expiringMessageManager.setExpirationTimer(message) + message.sentTimestamp = SnodeAPI.nowWithOffset + ApplicationContext.getInstance(this).expiringMessageManager.setExpirationTimer(message) MessageSender.send(message, thread.address) invalidateOptionsMenu() } } 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 @@ -1075,33 +1282,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 @@ -1127,7 +1338,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 @@ -1160,7 +1371,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) @@ -1351,15 +1562,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe 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) { @@ -1377,19 +1594,21 @@ 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() + message.sentTimestamp = sentTimestamp message.text = text val outgoingTextMessage = OutgoingTextMessage.from(message, recipient) // Clear the input bar @@ -1406,14 +1625,16 @@ 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? = null, 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() + message.sentTimestamp = sentTimestamp message.text = body val quote = quotedMessage?.let { val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf() @@ -1447,28 +1668,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) } @@ -1568,7 +1789,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) @@ -1615,35 +1836,23 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe 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) - } - endActionMode() + + 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() } + cancelButton { endActionMode() } } - builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> - dialog.dismiss() - endActionMode() - } - builder.show() } 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() } @@ -1654,54 +1863,32 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe bottomSheet.show(supportFragmentManager, bottomSheet.tag) } else { 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) - } - endActionMode() + + 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) } - 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().individualRecipient); endActionMode() } + cancelButton(::endActionMode) } - builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> - dialog.dismiss() - endActionMode() - } - builder.show() } override fun copyMessages(messages: Set) { @@ -1742,6 +1929,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) @@ -1749,16 +1943,30 @@ 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, { _, _ -> + SaveAttachmentTask.showWarningDialog(this) { Permissions.with(this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .maxSdkVersion(Build.VERSION_CODES.P) @@ -1786,12 +1994,12 @@ 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() } @@ -1810,7 +2018,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) @@ -1844,7 +2052,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (result == null) return@Observer if (result.getResults().isNotEmpty()) { result.getResults()[result.position]?.let { - jumpToMessage(it.messageRecipient.address, it.sentTimestampMs) { + jumpToMessage(it.messageRecipient.address, it.sentTimestampMs, true) { searchViewModel.onMissingResult() } } } @@ -1881,15 +2089,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() } @@ -1902,6 +2116,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) 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 85d3c8e6d..6013af5ba 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,6 +1,5 @@ package org.thoughtcrime.securesms.conversation.v2 -import android.app.AlertDialog import android.content.Context import android.content.Intent import android.database.Cursor @@ -31,10 +30,15 @@ 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, @@ -52,6 +56,8 @@ 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) + init { lifecycleCoroutineScope.launch(IO) { while (isActive) { @@ -128,6 +134,7 @@ class ConversationAdapter( searchQuery, contact, senderId, + lastSeen.get(), visibleMessageViewDelegate, onAttachmentNeedsDownload ) @@ -146,17 +153,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) @@ -185,14 +190,18 @@ 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 } @@ -219,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 } @@ -233,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 } @@ -243,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/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java index 995dcda2f..eee8b5ecd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java @@ -81,6 +81,8 @@ public final class ConversationReactionOverlay extends FrameLayout { private View dropdownAnchor; private LinearLayout conversationItem; + private View conversationBubble; + private TextView conversationTimestamp; private View backgroundView; private ConstraintLayout foregroundView; private EmojiImageView[] emojiViews; @@ -116,6 +118,8 @@ public final class ConversationReactionOverlay extends FrameLayout { 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); @@ -165,10 +169,8 @@ public final class ConversationReactionOverlay extends FrameLayout { 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); @@ -190,12 +192,8 @@ public final class ConversationReactionOverlay extends FrameLayout { } 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(); + if (message.isOutgoing()) conversationBubble.bringToFront(); + else conversationTimestamp.bringToFront(); } private void showAfterLayout(@NonNull MessageRecord messageRecord, @@ -203,10 +201,11 @@ public final class ConversationReactionOverlay extends FrameLayout { boolean isMessageOnLeft) { contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(messageRecord)); - float itemX = isMessageOnLeft ? scrubberHorizontalMargin : + float endX = isMessageOnLeft ? scrubberHorizontalMargin : selectedConversationModel.getBubbleX() - conversationItem.getWidth() + selectedConversationModel.getBubbleWidth(); - conversationItem.setX(itemX); - conversationItem.setY(selectedConversationModel.getBubbleY() - statusBarHeight); + float endY = selectedConversationModel.getBubbleY() - statusBarHeight; + conversationItem.setX(endX); + conversationItem.setY(endY); Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap(); boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth(); @@ -214,8 +213,6 @@ public final class ConversationReactionOverlay extends FrameLayout { int overlayHeight = getHeight(); int bubbleWidth = selectedConversationModel.getBubbleWidth(); - float endX = itemX; - float endY = conversationItem.getY(); float endApparentTop = endY; float endScale = 1f; @@ -265,9 +262,7 @@ public final class ConversationReactionOverlay extends FrameLayout { } } else { endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight(); - - float contextMenuTop = endY + conversationItemSnapshot.getHeight(); - reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY); + reactionBarBackgroundY = endY - reactionBarHeight - menuPadding; } endApparentTop = endY; @@ -354,11 +349,14 @@ public final class ConversationReactionOverlay extends FrameLayout { int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration); + conversationBubble.animate() + .scaleX(endScale) + .scaleY(endScale) + .setDuration(revealDuration); + conversationItem.animate() .x(endX) .y(endY) - .scaleX(endScale) - .scaleY(endScale) .setDuration(revealDuration); } @@ -660,10 +658,15 @@ public final class ConversationReactionOverlay extends FrameLayout { 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))); + items.add(new ActionItem(R.attr.menu_select_icon, getContext().getResources().getString(R.string.conversation_context__menu_select), () -> handleActionItemClicked(Action.SELECT), + getContext().getResources().getString(R.string.AccessibilityId_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))); + boolean canWrite = openGroup == null || openGroup.getCanWrite(); + if (canWrite && !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), + getContext().getResources().getString(R.string.AccessibilityId_reply_message)) + ); } // Copy message text if (!containsControlMessage && hasText) { @@ -671,11 +674,17 @@ public final class ConversationReactionOverlay extends FrameLayout { } // 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))); + 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))); + items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.delete), + () -> handleActionItemClicked(Action.DELETE), + getContext().getResources().getString(R.string.AccessibilityId_delete_message) + ) + ); } // Ban user if (ConversationMenuItemHelper.userCanBanSelectedUsers(getContext(), message, openGroup, userPublicKey, blindedPublicKey)) { @@ -686,16 +695,20 @@ public final class ConversationReactionOverlay extends FrameLayout { 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))); - } + 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))); } + // Resync + if (message.isSyncFailed()) { + items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resync_message), () -> handleActionItemClicked(Action.RESYNC))); + } // 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))); + items.add(new ActionItem(R.attr.menu_save_icon, getContext().getResources().getString(R.string.conversation_context_image__save_attachment), () -> handleActionItemClicked(Action.DOWNLOAD), + getContext().getResources().getString(R.string.AccessibilityId_save_attachment)) + ); } backgroundView.setVisibility(View.VISIBLE); @@ -876,6 +889,7 @@ public final class ConversationReactionOverlay extends FrameLayout { public enum Action { REPLY, RESEND, + RESYNC, DOWNLOAD, COPY_MESSAGE, COPY_SESSION_ID, 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 4a22113d2..94fe1e232 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,8 +1,10 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.content.ContentResolver import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import app.cash.copper.flow.observeQuery import com.goterl.lazysodium.utils.KeyPair import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -20,6 +22,8 @@ import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.DatabaseContentProviders +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.repository.ConversationRepository import java.util.UUID @@ -27,18 +31,28 @@ import java.util.UUID class ConversationViewModel( val threadId: Long, val edKeyPair: KeyPair?, + private val contentResolver: ContentResolver, private val repository: ConversationRepository, private val storage: StorageProtocol ) : 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 + private var _recipient: RetrieveOnce = RetrieveOnce { + repository.maybeGetRecipientForThreadId(threadId) + } val recipient: Recipient? - get() = repository.maybeGetRecipientForThreadId(threadId) + get() = _recipient.value + 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() @@ -49,6 +63,18 @@ class ConversationViewModel( ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString } + init { + viewModelScope.launch(Dispatchers.IO) { + contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)) + .collect { + val recipientExists = storage.getRecipientForThread(threadId) != null + if (!recipientExists && _uiState.value.conversationExists) { + _uiState.update { it.copy(conversationExists = false) } + } + } + } + } + fun saveDraft(text: String) { GlobalScope.launch(Dispatchers.IO) { repository.saveDraft(threadId, text) @@ -170,21 +196,26 @@ class ConversationViewModel( return repository.hasReceived(threadId) } + fun updateRecipient() { + _recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId)) + } + @dagger.assisted.AssistedFactory interface AssistedFactory { - fun create(threadId: Long, edKeyPair: KeyPair?): Factory + fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory } @Suppress("UNCHECKED_CAST") class Factory @AssistedInject constructor( @Assisted private val threadId: Long, @Assisted private val edKeyPair: KeyPair?, + @Assisted private val contentResolver: ContentResolver, private val repository: ConversationRepository, private val storage: StorageProtocol ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return ConversationViewModel(threadId, edKeyPair, repository, storage) as T + return ConversationViewModel(threadId, edKeyPair, contentResolver, repository, storage) as T } } } @@ -193,5 +224,22 @@ data class UiMessage(val id: Long, val message: String) data class ConversationUiState( 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 66f33cf29..b6212b854 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 d945b6a0a..b0d5e992a 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,98 +1,401 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.annotation.SuppressLint +import android.content.Intent import android.os.Bundle -import android.view.View -import androidx.core.view.isVisible +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.database.StorageProtocol -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.model.MessageRecord -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.DateUtils -import java.text.SimpleDateFormat -import java.util.* +import org.thoughtcrime.securesms.database.Storage +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: StorageProtocol - // 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) ?: run { - finish() - return - } - 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()) - if (errorMessage != null) { - binding.errorMessage.text = errorMessage - binding.resendContainer.isVisible = true - binding.errorContainer.isVisible = true - } else { - binding.errorContainer.isVisible = false - binding.resendContainer.isVisible = false - } - - 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 = { setResultAndFinish(ON_REPLY) }, + 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 = {}, + 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 = {}, + onResend: (() -> Unit)? = null, + onDelete: () -> Unit = {}, +) { + Cell { + Column { + ItemButton( + stringResource(R.string.reply), + R.drawable.ic_message_details__reply, + onClick = onReply + ) + 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 000000000..a73fe4113 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -0,0 +1,159 @@ +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.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +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.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, +) : ViewModel() { + + private val state = MutableStateFlow(MessageDetailsState()) + val stateFlow = state.asStateFlow() + + private val event = Channel() + val eventFlow = event.receiveAsFlow() + + var timestamp: Long = 0L + set(value) { + field = value + val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) + + if (record == null) { + viewModelScope.launch { event.send(Event.Finish) } + return + } + + val mmsRecord = record as? MmsMessageRecord + + 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 ?: return + 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) +} + +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 28c86b331..54deea1c8 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 4bff4e76a..6083bb267 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/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt index 834b77ecc..d54426391 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/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt index 39ca7c691..c0ff1cbb1 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 1799a6e87..ceb9410df 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,11 +1,12 @@ 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 @@ -13,7 +14,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment 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 javax.inject.Inject @@ -22,13 +23,12 @@ import javax.inject.Inject @AndroidEntryPoint class AutoDownloadDialog(private val threadRecipient: Recipient, private val databaseAttachment: DatabaseAttachment -) : BaseDialog() { +) : DialogFragment() { @Inject lateinit var storage: StorageProtocol @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 threadId = storage.getThreadId(threadRecipient) ?: run { dismiss() return @@ -39,25 +39,23 @@ class AutoDownloadDialog(private val threadRecipient: Recipient, threadRecipient.isClosedGroupRecipient -> storage.getGroup(threadRecipient.address.toGroupString())?.title ?: "UNKNOWN" else -> storage.getContactWithSessionID(threadRecipient.address.serialize())?.displayName(Contact.ContactContext.REGULAR) ?: "UNKNOWN" } - val title = resources.getString(R.string.dialog_auto_download_title) - binding.downloadTitleTextView.text = title + title(resources.getString(R.string.dialog_auto_download_title)) + val explanation = resources.getString(R.string.dialog_auto_download_explanation, displayName) val spannable = SpannableStringBuilder(explanation) - val startIndex = explanation.indexOf(displayName) - spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + displayName.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - binding.downloadExplanationTextView.text = spannable - binding.no.setOnClickListener { - setAutoDownload(false) - dismiss() - } - binding.yes.setOnClickListener { + val startIndex = explanation.indexOf(name) + spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + text(spannable) + + button(R.string.dialog_download_button_title, R.string.AccessibilityId_download_media) { setAutoDownload(true) - dismiss() } - builder.setView(binding.root) + cancelButton { + setAutoDownload(false) + } } private fun setAutoDownload(shouldDownload: Boolean) { storage.setAutoDownloadAttachments(threadRecipient, shouldDownload) } -} \ 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 444c389e0..a886e8919 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 a16ca86f7..996dd41f9 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 f51261d49..6abb0814d 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 7ac70b843..73e2d571c 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 @@ -57,9 +57,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() } 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 a21ba1b50..2d8f74596 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 401ccaa3c..e62f7f8f8 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 d475a6444..3746aa52e 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 @@ -67,9 +67,11 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p menu.findItem(R.id.menu_context_copy_public_key).isVisible = (thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey) // Message detail - menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing) + 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()) @@ -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) @@ -113,6 +116,7 @@ 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) 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 663dd2e25..02ee4ae45 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 @@ -14,7 +14,6 @@ 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 @@ -33,7 +32,6 @@ 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 +42,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.showSessionDialog +import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.util.BitmapUtil import java.io.IOException @@ -63,26 +63,31 @@ object ConversationMenuHelper { // Base menu (options that should always be present) inflater.inflate(R.menu.menu_conversation, menu) // Expiring messages - if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient)) { + if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient) && !thread.isBlocked) { 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) } + item.actionView?.let { 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) } } + // 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) } } @@ -154,6 +159,7 @@ object ConversationMenuHelper { 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 +186,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) } @@ -270,6 +273,12 @@ object ConversationMenuHelper { listener.copySessionID(thread.address.toString()) } + private fun copyOpenGroupUrl(context: Context, thread: Recipient) { + if (!thread.isOpenGroupRecipient) { 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 +289,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,29 +299,25 @@ 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) { @@ -329,7 +332,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,6 +347,7 @@ object ConversationMenuHelper { fun block(deleteThread: Boolean = false) fun unblock() fun copySessionID(sessionId: String) + fun copyOpenGroupUrl(thread: Recipient) fun showExpiringMessagesDialog(thread: Recipient) } 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 a4e4a52d5..3e370104e 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 @@ -31,6 +31,7 @@ class ControlMessageView : LinearLayout { binding.dateBreakTextView.showDateBreak(message, previous) binding.iconImageView.visibility = View.GONE var messageBody: CharSequence = message.getDisplayBody(context) + binding.root.contentDescription= null when { message.isExpirationTimerUpdate -> { binding.iconImageView.setImageDrawable( @@ -46,6 +47,7 @@ class ControlMessageView : LinearLayout { } message.isMessageRequestResponse -> { messageBody = context.getString(R.string.message_requests_accepted) + binding.root.contentDescription=context.getString(R.string.AccessibilityId_message_request_config_message) } message.isCallLog -> { val drawable = when { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index 45d353cc3..967722389 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -57,8 +57,12 @@ class LinkPreviewView : LinearLayout { val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopRightRadius(cornerRadii[1]) - cornerMask.setBottomRightRadius(cornerRadii[2]) - cornerMask.setBottomLeftRadius(cornerRadii[3]) + + // Only round the bottom corners if there is no body text + if (message.body.isEmpty()) { + cornerMask.setBottomRightRadius(cornerRadii[2]) + cornerMask.setBottomLeftRadius(cornerRadii[3]) + } } override fun dispatchDraw(canvas: Canvas) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 060cd0b04..d23656616 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.Context import android.graphics.Color import android.graphics.Rect -import android.graphics.drawable.Drawable import android.text.Spannable import android.text.style.BackgroundColorSpan import android.text.style.ForegroundColorSpan @@ -15,11 +14,10 @@ import android.view.View import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.res.ResourcesCompat -import androidx.core.graphics.BlendModeColorFilterCompat -import androidx.core.graphics.BlendModeCompat +import androidx.core.graphics.ColorUtils import androidx.core.text.getSpans import androidx.core.text.toSpannable +import androidx.core.view.children import androidx.core.view.isVisible import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding @@ -27,6 +25,7 @@ import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 @@ -36,7 +35,10 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.database.model.SmsMessageRecord +import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.getAccentColor import java.util.Locale @@ -44,7 +46,6 @@ import kotlin.math.roundToInt class VisibleMessageContentView : ConstraintLayout { private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) } - var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf() var onContentDoubleTap: (() -> Unit)? = null var delegate: VisibleMessageViewDelegate? = null var indexInAdapter: Int = -1 @@ -58,20 +59,20 @@ class VisibleMessageContentView : ConstraintLayout { // region Updating fun bind( message: MessageRecord, - isStartOfMessageCluster: Boolean, - isEndOfMessageCluster: Boolean, - glide: GlideRequests, + isStartOfMessageCluster: Boolean = true, + isEndOfMessageCluster: Boolean = true, + glide: GlideRequests = GlideApp.with(this), thread: Recipient, - searchQuery: String?, - onAttachmentNeedsDownload: (Long, Long) -> Unit + searchQuery: String? = null, + contactIsTrusted: Boolean = true, + onAttachmentNeedsDownload: (Long, Long) -> Unit, + suppressThumbnails: Boolean = false ) { // Background - val background = getBackground(message.isOutgoing) val color = if (message.isOutgoing) context.getAccentColor() else context.getColorFromAttr(R.attr.message_received_background_color) - val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN) - background.colorFilter = filter - binding.contentParent.background = background + binding.contentParent.mainColor = color + binding.contentParent.cornerRadius = resources.getDimension(R.dimen.message_corner_radius) val mediaDownloaded = message is MmsMessageRecord && message.slideDeck.asAttachments().all { it.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE } val mediaInProgress = message is MmsMessageRecord && message.slideDeck.asAttachments().any { it.isInProgress } @@ -97,6 +98,9 @@ class VisibleMessageContentView : ConstraintLayout { binding.deletedMessageView.root.isVisible = false } + // Note: Need to clear the body to prevent the message bubble getting incorrectly + // sized based on text content from a recycled view + binding.bodyTextView.text = null binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null binding.linkPreviewView.root.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty() binding.pendingAttachmentView.root.isVisible = !mediaDownloaded && !mediaInProgress && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty() @@ -125,7 +129,6 @@ class VisibleMessageContentView : ConstraintLayout { delegate?.scrollToMessageIfPossible(quote.id) } } - val hasMedia = message.slideDeck.asAttachments().isNotEmpty() } if (message is MmsMessageRecord) { @@ -152,7 +155,9 @@ class VisibleMessageContentView : ConstraintLayout { message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> { binding.linkPreviewView.root.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) onContentClick.add { event -> binding.linkPreviewView.root.calculateHit(event) } - // Body text view is inside the link preview for layout convenience + + // When in a link preview ensure the bodyTextView can expand to the full width + binding.bodyTextView.maxWidth = binding.linkPreviewView.root.layoutParams.width } // AUDIO message is MmsMessageRecord && message.slideDeck.audioSlide != null -> { @@ -197,7 +202,7 @@ class VisibleMessageContentView : ConstraintLayout { } } // IMAGE / VIDEO - message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty() -> { + message is MmsMessageRecord && !suppressThumbnails && message.slideDeck.asAttachments().isNotEmpty() -> { if (mediaDownloaded || mediaInProgress || message.isOutgoing) { // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups // bind after add view because views are inflated and calculated during bind @@ -237,6 +242,7 @@ class VisibleMessageContentView : ConstraintLayout { } binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody + binding.contentParent.apply { isVisible = children.any { it.isVisible } } if (message.body.isNotEmpty() && !hideBody) { val color = getTextColor(context, message) @@ -255,14 +261,15 @@ class VisibleMessageContentView : ConstraintLayout { binding.contentParent.layoutParams = layoutParams } + private val onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf() + + fun onContentClick(event: MotionEvent) { + onContentClick.forEach { clickHandler -> clickHandler.invoke(event) } + } + private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean = listOf(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible } - private fun getBackground(isOutgoing: Boolean): Drawable { - val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone - return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!! - } - fun recycle() { arrayOf( binding.deletedMessageView.root, @@ -280,6 +287,15 @@ class VisibleMessageContentView : ConstraintLayout { fun playVoiceMessage() { binding.voiceMessageView.root.togglePlayback() } + + fun playHighlight() { + // Show the highlight colour immediately then slowly fade out + val targetColor = if (ThemeUtil.isDarkTheme(context)) context.getAccentColor() else resources.getColor(R.color.black, context.theme) + val clearTargetColor = ColorUtils.setAlphaComponent(targetColor, 0) + binding.contentParent.numShadowRenders = if (ThemeUtil.isDarkTheme(context)) 3 else 1 + binding.contentParent.sessionShadowColor = targetColor + GlowViewUtilities.animateShadowColorChange(binding.contentParent, targetColor, clearTargetColor, 1600) + } // endregion // region Convenience diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 5f730d311..352191339 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -2,17 +2,21 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.Context import android.content.Intent -import android.content.res.Resources import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.ColorDrawable import android.os.Handler import android.os.Looper import android.util.AttributeSet +import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View +import android.widget.FrameLayout import android.widget.LinearLayout +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat @@ -26,6 +30,7 @@ import network.loki.messenger.databinding.ViewVisibleMessageBinding import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.getColorFromAttr @@ -42,6 +47,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.home.UserDetailsBottomSheet +import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.disableClipping @@ -66,7 +72,6 @@ class VisibleMessageView : LinearLayout { @Inject lateinit var mmsDb: MmsDatabase private val binding by lazy { ViewVisibleMessageBinding.bind(this) } - private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate() private val swipeToReplyIconRect = Rect() private var dx = 0.0f @@ -107,7 +112,10 @@ class VisibleMessageView : LinearLayout { private fun initialize() { isHapticFeedbackEnabled = true setWillNotDraw(false) + binding.root.disableClipping() + binding.mainContainer.disableClipping() binding.messageInnerContainer.disableClipping() + binding.messageInnerLayout.disableClipping() binding.messageContentView.root.disableClipping() } // endregion @@ -115,13 +123,14 @@ class VisibleMessageView : LinearLayout { // region Updating fun bind( message: MessageRecord, - previous: MessageRecord?, - next: MessageRecord?, - glide: GlideRequests, - searchQuery: String?, - contact: Contact?, + previous: MessageRecord? = null, + next: MessageRecord? = null, + glide: GlideRequests = GlideApp.with(this), + searchQuery: String? = null, + contact: Contact? = null, senderSessionID: String, - delegate: VisibleMessageViewDelegate?, + lastSeen: Long, + delegate: VisibleMessageViewDelegate? = null, onAttachmentNeedsDownload: (Long, Long) -> Unit ) { val threadID = message.threadId @@ -132,7 +141,7 @@ class VisibleMessageView : LinearLayout { // Show profile picture and sender name if this is a group thread AND // the message is incoming binding.moderatorIconImageView.isVisible = false - binding.profilePictureView.root.visibility = when { + binding.profilePictureView.visibility = when { thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE thread.isGroupRecipient -> View.INVISIBLE else -> View.GONE @@ -141,25 +150,25 @@ class VisibleMessageView : LinearLayout { val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing) else ViewUtil.dpToPx(context,2) - if (binding.profilePictureView.root.visibility == View.GONE) { + if (binding.profilePictureView.visibility == View.GONE) { val expirationParams = binding.messageInnerContainer.layoutParams as MarginLayoutParams expirationParams.bottomMargin = bottomMargin binding.messageInnerContainer.layoutParams = expirationParams } else { - val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams + val avatarLayoutParams = binding.profilePictureView.layoutParams as MarginLayoutParams avatarLayoutParams.bottomMargin = bottomMargin - binding.profilePictureView.root.layoutParams = avatarLayoutParams + binding.profilePictureView.layoutParams = avatarLayoutParams } if (isGroupThread && !message.isOutgoing) { if (isEndOfMessageCluster) { - binding.profilePictureView.root.publicKey = senderSessionID - binding.profilePictureView.root.glide = glide - binding.profilePictureView.root.update(message.individualRecipient) - binding.profilePictureView.root.setOnClickListener { + binding.profilePictureView.publicKey = senderSessionID + binding.profilePictureView.update(message.individualRecipient) + binding.profilePictureView.setOnClickListener { if (thread.isOpenGroupRecipient) { val openGroup = lokiThreadDb.getOpenGroupChat(threadID) if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) { + // TODO: support v2 soon val intent = Intent(context, ConversationActivityV2::class.java) intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID) intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID)) @@ -173,7 +182,7 @@ class VisibleMessageView : LinearLayout { val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return var standardPublicKey = "" var blindedPublicKey: String? = null - if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) { + if (IdPrefix.fromValue(senderSessionID)?.isBlinded() == true) { blindedPublicKey = senderSessionID } else { standardPublicKey = senderSessionID @@ -187,13 +196,15 @@ class VisibleMessageView : LinearLayout { val contactContext = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID + // Unread marker + binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing // Date break val showDateBreak = isStartOfMessageCluster || snIsSelected binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null binding.dateBreakTextView.isVisible = showDateBreak // Message status indicator if (message.isOutgoing) { - val (iconID, iconColor, textId) = getMessageStatusImage(message) + val (iconID, iconColor, textId, contentDescription) = getMessageStatusImage(message) if (textId != null) { binding.messageStatusTextView.setText(textId) @@ -208,6 +219,7 @@ class VisibleMessageView : LinearLayout { } binding.messageStatusImageView.setImageDrawable(drawable) } + binding.messageStatusImageView.contentDescription = contentDescription val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId) binding.messageStatusTextView.isVisible = ( @@ -281,29 +293,63 @@ class VisibleMessageView : LinearLayout { } } - private fun getMessageStatusImage(message: MessageRecord): Triple { - return when { - !message.isOutgoing -> Triple(null, null, null) - message.isFailed -> - Triple(R.drawable.ic_delivery_status_failed, resources.getColor(R.color.destructive, context.theme), R.string.delivery_status_failed) - message.isPending -> - Triple(R.drawable.ic_delivery_status_sending, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending) - message.isRead -> - Triple(R.drawable.ic_delivery_status_read, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read) - else -> - Triple(R.drawable.ic_delivery_status_sent, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sent) - } + data class MessageStatusInfo(@DrawableRes val iconId: Int?, + @ColorInt val iconTint: Int?, + @StringRes val messageText: Int?, + val contentDescription: String?) + + private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when { + message.isFailed -> + MessageStatusInfo( + R.drawable.ic_delivery_status_failed, + resources.getColor(R.color.destructive, context.theme), + R.string.delivery_status_failed, + null + ) + message.isSyncFailed -> + MessageStatusInfo( + R.drawable.ic_delivery_status_failed, + context.getColor(R.color.accent_orange), + R.string.delivery_status_sync_failed, + null + ) + message.isPending -> + MessageStatusInfo( + R.drawable.ic_delivery_status_sending, + context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending, + context.getString(R.string.AccessibilityId_message_sent_status_pending) + ) + message.isResyncing -> + MessageStatusInfo( + R.drawable.ic_delivery_status_sending, + context.getColor(R.color.accent_orange), R.string.delivery_status_syncing, + context.getString(R.string.AccessibilityId_message_sent_status_syncing) + ) + message.isRead -> + MessageStatusInfo( + R.drawable.ic_delivery_status_read, + context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read, + null + ) + else -> + MessageStatusInfo( + R.drawable.ic_delivery_status_sent, + context.getColorFromAttr(R.attr.message_status_color), + R.string.delivery_status_sent, + context.getString(R.string.AccessibilityId_message_sent_status_tick) + ) } private fun updateExpirationTimer(message: MessageRecord) { val container = binding.messageInnerContainer - val content = binding.messageContentView.root - val expiration = binding.expirationTimerView - val spacing = binding.messageContentSpacing - container.removeAllViewsInLayout() - container.addView(if (message.isOutgoing) expiration else content) - container.addView(if (message.isOutgoing) content else expiration) - container.addView(spacing, if (message.isOutgoing) 0 else 2) + val layout = binding.messageInnerLayout + + if (message.isOutgoing) binding.messageContentView.root.bringToFront() + else binding.expirationTimerView.bringToFront() + + layout.layoutParams = layout.layoutParams.let { it as FrameLayout.LayoutParams } + .apply { gravity = if (message.isOutgoing) Gravity.END else Gravity.START } + val containerParams = container.layoutParams as ConstraintLayout.LayoutParams containerParams.horizontalBias = if (message.isOutgoing) 1f else 0f container.layoutParams = containerParams @@ -314,7 +360,7 @@ class VisibleMessageView : LinearLayout { if (message.expireStarted > 0) { binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) binding.expirationTimerView.startAnimation() - if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) { + if (message.expireStarted + message.expiresIn <= SnodeAPI.nowWithOffset) { ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule() } } else if (!message.isMediaPending) { @@ -349,7 +395,7 @@ class VisibleMessageView : LinearLayout { val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val iconSize = toPx(24, context.resources) val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing - val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2) + val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.marginBottom - (iconSize / 2) val right = left + iconSize val bottom = top + iconSize swipeToReplyIconRect.left = left @@ -369,9 +415,13 @@ class VisibleMessageView : LinearLayout { } fun recycle() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() binding.messageContentView.root.recycle() } + + fun playHighlight() { + binding.messageContentView.root.playHighlight() + } // endregion // region Interaction @@ -466,7 +516,7 @@ class VisibleMessageView : LinearLayout { } fun onContentClick(event: MotionEvent) { - binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) } + binding.messageContentView.root.onContentClick(event) } private fun onPress(event: MotionEvent) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index e1bf92c5f..2b829af15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -92,7 +92,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener { if (progress == 1.0) { togglePlayback() handleProgressChanged(0.0) - delegate?.playVoiceMessageAtIndexIfPossible(indexInAdapter - 1) + delegate?.playVoiceMessageAtIndexIfPossible(indexInAdapter + 1) } else { handleProgressChanged(progress) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java index dd90b699e..088685241 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.provider.OpenableColumns; import android.text.TextUtils; import android.util.Pair; @@ -244,12 +245,17 @@ public class AttachmentManager { } public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) { - Permissions.with(activity) - .request(Manifest.permission.READ_EXTERNAL_STORAGE) - .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) - .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24) - .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) - .execute(); + Permissions.PermissionsBuilder builder = Permissions.with(activity); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO) + .request(Manifest.permission.READ_MEDIA_IMAGES); + } else { + builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE); + } + builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) + .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24) + .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) + .execute(); } public static void selectAudio(Activity activity, int requestCode) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt deleted file mode 100644 index e1456a7f9..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.utilities - -import android.app.Dialog -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment -import org.thoughtcrime.securesms.util.UiModeUtilities - -open class BaseDialog : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = AlertDialog.Builder(requireContext()) - setContentView(builder) - val result = builder.create() - result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - val isLightMode = UiModeUtilities.isDayUiMode(requireContext()) - result.window?.setDimAmount(if (isLightMode) 0.1f else 0.75f) - return result - } - - open fun setContentView(builder: AlertDialog.Builder) { - // To be overridden by subclasses - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt index dbbcfb51e..c0ce83f63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt @@ -1,21 +1,18 @@ package org.thoughtcrime.securesms.conversation.v2.utilities import android.content.Context -import androidx.appcompat.app.AlertDialog import network.loki.messenger.R import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.showSessionDialog object NotificationUtils { fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) { - val notifyTypes = context.resources.getStringArray(R.array.notify_types) - val currentSelected = thread.notifyType - - AlertDialog.Builder(context) - .setSingleChoiceItems(notifyTypes,currentSelected) { d, newSelection -> - notifyTypeHandler(newSelection) - d.dismiss() - } - .setTitle(R.string.RecipientPreferenceActivity_notification_settings) - .show() + context.showSessionDialog { + title(R.string.RecipientPreferenceActivity_notification_settings) + singleChoiceItems( + context.resources.getStringArray(R.array.notify_types), + thread.notifyType + ) { notifyTypeHandler(it) } + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt index 80f4cc0bf..e01a75b30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.utilities import android.content.Context import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.visible.LinkPreview import org.session.libsession.messaging.messages.visible.OpenGroupInvitation import org.session.libsession.messaging.messages.visible.Quote @@ -15,7 +16,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord object ResendMessageUtilities { - fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?) { + fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { val recipient: Recipient = messageRecord.recipient val message = VisibleMessage() message.id = messageRecord.getId() @@ -55,8 +56,13 @@ object ResendMessageUtilities { val sentTimestamp = message.sentTimestamp val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey() if (sentTimestamp != null && sender != null) { - MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender) + if (isResync) { + MessagingModuleConfiguration.shared.storage.markAsResyncing(sentTimestamp, sender) + MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = true) + } else { + MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender) + MessageSender.send(message, recipient.address) + } } - MessageSender.send(message, recipient.address) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt index 800ace54c..7a47b9275 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt @@ -38,13 +38,12 @@ object TextUtilities { fun TextView.getIntersectedModalSpans(hitRect: Rect): List { val textLayout = layout ?: return emptyList() val lineRect = Rect() - val bodyTextRect = Rect() - getGlobalVisibleRect(bodyTextRect) + val offset = intArrayOf(0, 0).also { getLocationOnScreen(it) } val textSpannable = text.toSpannable() return (0 until textLayout.lineCount).flatMap { line -> textLayout.getLineBounds(line, lineRect) - lineRect.offset(bodyTextRect.left + totalPaddingLeft, bodyTextRect.top + totalPaddingTop) - if ((Rect(lineRect)).contains(hitRect)) { + lineRect.offset(offset[0] + totalPaddingLeft, offset[1] + totalPaddingTop) + if (lineRect.contains(hitRect)) { // calculate the url span intersected with (if any) val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same textSpannable.getSpans(off, off).toList() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt index e15855667..4a9986d6e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt @@ -123,10 +123,10 @@ open class ThumbnailView: FrameLayout { when { slide.thumbnailUri != null -> { - buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, result)) + buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, result)) } slide.hasPlaceholder() -> { - buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, result)) + buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, null, result)) } else -> { glide.clear(binding.thumbnailImage) @@ -190,7 +190,7 @@ open class ThumbnailView: FrameLayout { request.transforms(CenterCrop()) } - request.into(GlideDrawableListeningTarget(binding.thumbnailImage, future)) + request.into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, future)) return future } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java index a5333ef5d..62aaf58f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java @@ -52,6 +52,7 @@ public class IdentityKeyUtil { public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3"; public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key"; public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key"; + public static final String NOTIFICATION_KEY = "pref_notification_key"; public static final String LOKI_SEED = "loki_seed"; public static final String HAS_MIGRATED_KEY = "has_migrated_keys"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java index 43e986559..7f0edddeb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java @@ -1,13 +1,13 @@ package org.thoughtcrime.securesms.crypto; -import android.os.Build; +import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; + import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.util.Base64; import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; @@ -45,44 +45,50 @@ public final class KeyStoreHelper { private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; private static final String KEY_ALIAS = "SignalSecret"; - @RequiresApi(Build.VERSION_CODES.M) public static SealedData seal(@NonNull byte[] input) { SecretKey secretKey = getOrCreateKeyStoreEntry(); try { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); + // Cipher operations are not thread-safe so we synchronize over them through doFinal to + // prevent crashes with quickly repeated encrypt/decrypt operations + // https://github.com/mozilla-mobile/android-components/issues/5342 + synchronized (CIPHER_LOCK) { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] iv = cipher.getIV(); - byte[] data = cipher.doFinal(input); + byte[] iv = cipher.getIV(); + byte[] data = cipher.doFinal(input); - return new SealedData(iv, data); + return new SealedData(iv, data); + } } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } } - @RequiresApi(Build.VERSION_CODES.M) public static byte[] unseal(@NonNull SealedData sealedData) { SecretKey secretKey = getKeyStoreEntry(); try { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv)); + // Cipher operations are not thread-safe so we synchronize over them through doFinal to + // prevent crashes with quickly repeated encrypt/decrypt operations + // https://github.com/mozilla-mobile/android-components/issues/5342 + synchronized (CIPHER_LOCK) { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv)); - return cipher.doFinal(sealedData.data); + return cipher.doFinal(sealedData.data); + } } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } } - @RequiresApi(Build.VERSION_CODES.M) private static SecretKey getOrCreateKeyStoreEntry() { if (hasKeyStoreEntry()) return getKeyStoreEntry(); else return createKeyStoreEntry(); } - @RequiresApi(Build.VERSION_CODES.M) private static SecretKey createKeyStoreEntry() { try { KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE); @@ -99,7 +105,6 @@ public final class KeyStoreHelper { } } - @RequiresApi(Build.VERSION_CODES.M) private static SecretKey getKeyStoreEntry() { KeyStore keyStore = getKeyStore(); @@ -137,7 +142,6 @@ public final class KeyStoreHelper { } } - @RequiresApi(Build.VERSION_CODES.M) private static boolean hasKeyStoreEntry() { try { KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE); @@ -202,7 +206,5 @@ public final class KeyStoreHelper { return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING); } } - } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt new file mode 100644 index 000000000..19a511bfd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt @@ -0,0 +1,53 @@ +package org.thoughtcrime.securesms.database + +import android.content.Context +import androidx.core.content.contentValuesOf +import androidx.core.database.getBlobOrNull +import androidx.core.database.getLongOrNull +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper + +class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) { + + companion object { + private const val VARIANT = "variant" + private const val PUBKEY = "publicKey" + private const val DATA = "data" + private const val TIMESTAMP = "timestamp" // Milliseconds + + private const val TABLE_NAME = "configs_table" + + const val CREATE_CONFIG_TABLE_COMMAND = + "CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $TIMESTAMP INTEGER NOT NULL DEFAULT 0, PRIMARY KEY($VARIANT, $PUBKEY));" + + private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?" + } + + fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) { + val db = writableDatabase + val contentValues = contentValuesOf( + VARIANT to variant, + PUBKEY to publicKey, + DATA to data, + TIMESTAMP to timestamp + ) + db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey)) + } + + fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? { + val db = readableDatabase + val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) + return query?.use { cursor -> + if (!cursor.moveToFirst()) return@use null + val bytes = cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) ?: return@use null + bytes + } + } + + fun retrieveConfigLastUpdateTimestamp(variant: String, publicKey: String): Long { + val db = readableDatabase + val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) + if (cursor == null) return 0 + if (!cursor.moveToFirst()) return 0 + return (cursor.getLongOrNull(cursor.getColumnIndex(TIMESTAMP)) ?: 0) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/FastCursorRecyclerViewAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/database/FastCursorRecyclerViewAdapter.java deleted file mode 100644 index 4dfe6a20b..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/FastCursorRecyclerViewAdapter.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.thoughtcrime.securesms.database; - - -import android.content.Context; -import android.database.Cursor; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -public abstract class FastCursorRecyclerViewAdapter - extends CursorRecyclerViewAdapter -{ - private static final String TAG = FastCursorRecyclerViewAdapter.class.getSimpleName(); - - private final LinkedList fastRecords = new LinkedList<>(); - private final List releasedRecordIds = new LinkedList<>(); - - protected FastCursorRecyclerViewAdapter(Context context, Cursor cursor) { - super(context, cursor); - } - - public void addFastRecord(@NonNull T record) { - fastRecords.addFirst(record); - notifyDataSetChanged(); - } - - public void releaseFastRecord(long id) { - synchronized (releasedRecordIds) { - releasedRecordIds.add(id); - } - } - - protected void cleanFastRecords() { - synchronized (releasedRecordIds) { - Iterator releaseIdIterator = releasedRecordIds.iterator(); - - while (releaseIdIterator.hasNext()) { - long releasedId = releaseIdIterator.next(); - Iterator fastRecordIterator = fastRecords.iterator(); - - while (fastRecordIterator.hasNext()) { - if (isRecordForId(fastRecordIterator.next(), releasedId)) { - fastRecordIterator.remove(); - releaseIdIterator.remove(); - break; - } - } - } - } - } - - protected abstract T getRecordFromCursor(@NonNull Cursor cursor); - protected abstract void onBindItemViewHolder(VH viewHolder, @NonNull T record); - protected abstract long getItemId(@NonNull T record); - protected abstract int getItemViewType(@NonNull T record); - protected abstract boolean isRecordForId(@NonNull T record, long id); - - @Override - public int getItemViewType(@NonNull Cursor cursor) { - T record = getRecordFromCursor(cursor); - return getItemViewType(record); - } - - @Override - public void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor) { - T record = getRecordFromCursor(cursor); - onBindItemViewHolder(viewHolder, record); - } - - @Override - public void onBindFastAccessItemViewHolder(VH viewHolder, int position) { - int calculatedPosition = getCalculatedPosition(position); - onBindItemViewHolder(viewHolder, fastRecords.get(calculatedPosition)); - } - - @Override - protected int getFastAccessSize() { - return fastRecords.size(); - } - - protected T getRecordForPositionOrThrow(int position) { - if (isFastAccessPosition(position)) { - return fastRecords.get(getCalculatedPosition(position)); - } else { - Cursor cursor = getCursorAtPositionOrThrow(position); - return getRecordFromCursor(cursor); - } - } - - protected int getFastAccessItemViewType(int position) { - return getItemViewType(fastRecords.get(getCalculatedPosition(position))); - } - - protected boolean isFastAccessPosition(int position) { - position = getCalculatedPosition(position); - return position >= 0 && position < fastRecords.size(); - } - - protected long getFastAccessItemId(int position) { - return getItemId(fastRecords.get(getCalculatedPosition(position))); - } - - private int getCalculatedPosition(int position) { - return hasHeaderView() ? position - 1 : position; - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 584bf3a71..66d01114e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -36,9 +36,9 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt @SuppressWarnings("unused") private static final String TAG = GroupDatabase.class.getSimpleName(); - static final String TABLE_NAME = "groups"; + public static final String TABLE_NAME = "groups"; private static final String ID = "_id"; - static final String GROUP_ID = "group_id"; + public static final String GROUP_ID = "group_id"; private static final String TITLE = "title"; private static final String MEMBERS = "members"; private static final String ZOMBIE_MEMBERS = "zombie_members"; @@ -133,12 +133,12 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt return new Reader(cursor); } - public List getAllGroups() { + public List getAllGroups(boolean includeInactive) { Reader reader = getGroups(); GroupRecord record; List groups = new LinkedList<>(); while ((record = reader.getNext()) != null) { - if (record.isActive()) { groups.add(record); } + if (record.isActive() || includeInactive) { groups.add(record); } } reader.close(); return groups; @@ -318,6 +318,25 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt notifyConversationListListeners(); } + @Override + public void removeProfilePicture(String groupID) { + databaseHelper.getWritableDatabase() + .execSQL("UPDATE " + TABLE_NAME + + " SET " + AVATAR + " = NULL, " + + AVATAR_ID + " = NULL, " + + AVATAR_KEY + " = NULL, " + + AVATAR_CONTENT_TYPE + " = NULL, " + + AVATAR_RELAY + " = NULL, " + + AVATAR_DIGEST + " = NULL, " + + AVATAR_URL + " = NULL" + + " WHERE " + + GROUP_ID + " = ?", + new String[] {groupID}); + + Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(null)); + notifyConversationListListeners(); + } + public boolean hasDownloadedProfilePicture(String groupId) { try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{AVATAR}, GROUP_ID + " = ?", new String[] {groupId}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.java deleted file mode 100644 index ef4746923..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.java +++ /dev/null @@ -1,249 +0,0 @@ -package org.thoughtcrime.securesms.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import androidx.annotation.NonNull; - -import net.zetetic.database.sqlcipher.SQLiteDatabase; - -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec; -import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec; - -import java.util.LinkedList; -import java.util.List; - -public class JobDatabase extends Database { - - public static final String[] CREATE_TABLE = new String[] { Jobs.CREATE_TABLE, - Constraints.CREATE_TABLE, - Dependencies.CREATE_TABLE }; - - public static final class Jobs { - public static final String TABLE_NAME = "job_spec"; - private static final String ID = "_id"; - private static final String JOB_SPEC_ID = "job_spec_id"; - private static final String FACTORY_KEY = "factory_key"; - private static final String QUEUE_KEY = "queue_key"; - private static final String CREATE_TIME = "create_time"; - private static final String NEXT_RUN_ATTEMPT_TIME = "next_run_attempt_time"; - private static final String RUN_ATTEMPT = "run_attempt"; - private static final String MAX_ATTEMPTS = "max_attempts"; - private static final String MAX_BACKOFF = "max_backoff"; - private static final String MAX_INSTANCES = "max_instances"; - private static final String LIFESPAN = "lifespan"; - private static final String SERIALIZED_DATA = "serialized_data"; - private static final String IS_RUNNING = "is_running"; - - private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - JOB_SPEC_ID + " TEXT UNIQUE, " + - FACTORY_KEY + " TEXT, " + - QUEUE_KEY + " TEXT, " + - CREATE_TIME + " INTEGER, " + - NEXT_RUN_ATTEMPT_TIME + " INTEGER, " + - RUN_ATTEMPT + " INTEGER, " + - MAX_ATTEMPTS + " INTEGER, " + - MAX_BACKOFF + " INTEGER, " + - MAX_INSTANCES + " INTEGER, " + - LIFESPAN + " INTEGER, " + - SERIALIZED_DATA + " TEXT, " + - IS_RUNNING + " INTEGER)"; - } - - public static final class Constraints { - public static final String TABLE_NAME = "constraint_spec"; - private static final String ID = "_id"; - private static final String JOB_SPEC_ID = "job_spec_id"; - private static final String FACTORY_KEY = "factory_key"; - - private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - JOB_SPEC_ID + " TEXT, " + - FACTORY_KEY + " TEXT, " + - "UNIQUE(" + JOB_SPEC_ID + ", " + FACTORY_KEY + "))"; - } - - public static final class Dependencies { - public static final String TABLE_NAME = "dependency_spec"; - private static final String ID = "_id"; - private static final String JOB_SPEC_ID = "job_spec_id"; - private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id"; - - private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - JOB_SPEC_ID + " TEXT, " + - DEPENDS_ON_JOB_SPEC_ID + " TEXT, " + - "UNIQUE(" + JOB_SPEC_ID + ", " + DEPENDS_ON_JOB_SPEC_ID + "))"; - } - - - public JobDatabase(Context context, SQLCipherOpenHelper databaseHelper) { - super(context, databaseHelper); - } - - public synchronized void insertJobs(@NonNull List fullSpecs) { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - - db.beginTransaction(); - - try { - for (FullSpec fullSpec : fullSpecs) { - insertJobSpec(db, fullSpec.getJobSpec()); - insertConstraintSpecs(db, fullSpec.getConstraintSpecs()); - insertDependencySpecs(db, fullSpec.getDependencySpecs()); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - public synchronized @NonNull List getAllJobSpecs() { - List jobs = new LinkedList<>(); - - try (Cursor cursor = databaseHelper.getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) { - while (cursor != null && cursor.moveToNext()) { - jobs.add(jobSpecFromCursor(cursor)); - } - } - - return jobs; - } - - public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Jobs.IS_RUNNING, isRunning ? 1 : 0); - - String query = Jobs.JOB_SPEC_ID + " = ?"; - String[] args = new String[]{ id }; - - databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args); - } - - public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Jobs.IS_RUNNING, isRunning ? 1 : 0); - contentValues.put(Jobs.RUN_ATTEMPT, runAttempt); - contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, nextRunAttemptTime); - - String query = Jobs.JOB_SPEC_ID + " = ?"; - String[] args = new String[]{ id }; - - databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args); - } - - public synchronized void updateAllJobsToBePending() { - ContentValues contentValues = new ContentValues(); - contentValues.put(Jobs.IS_RUNNING, 0); - - databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null); - } - - public synchronized void deleteJobs(@NonNull List jobIds) { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - - db.beginTransaction(); - - try { - for (String jobId : jobIds) { - String[] arg = new String[]{jobId}; - - db.delete(Jobs.TABLE_NAME, Jobs.JOB_SPEC_ID + " = ?", arg); - db.delete(Constraints.TABLE_NAME, Constraints.JOB_SPEC_ID + " = ?", arg); - db.delete(Dependencies.TABLE_NAME, Dependencies.JOB_SPEC_ID + " = ?", arg); - db.delete(Dependencies.TABLE_NAME, Dependencies.DEPENDS_ON_JOB_SPEC_ID + " = ?", arg); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - public synchronized @NonNull List getAllConstraintSpecs() { - List constraints = new LinkedList<>(); - - try (Cursor cursor = databaseHelper.getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - constraints.add(constraintSpecFromCursor(cursor)); - } - } - - return constraints; - } - - public synchronized @NonNull List getAllDependencySpecs() { - List dependencies = new LinkedList<>(); - - try (Cursor cursor = databaseHelper.getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - dependencies.add(dependencySpecFromCursor(cursor)); - } - } - - return dependencies; - } - - private void insertJobSpec(@NonNull SQLiteDatabase db, @NonNull JobSpec job) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Jobs.JOB_SPEC_ID, job.getId()); - contentValues.put(Jobs.FACTORY_KEY, job.getFactoryKey()); - contentValues.put(Jobs.QUEUE_KEY, job.getQueueKey()); - contentValues.put(Jobs.CREATE_TIME, job.getCreateTime()); - contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime()); - contentValues.put(Jobs.RUN_ATTEMPT, job.getRunAttempt()); - contentValues.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts()); - contentValues.put(Jobs.MAX_BACKOFF, job.getMaxBackoff()); - contentValues.put(Jobs.MAX_INSTANCES, job.getMaxInstances()); - contentValues.put(Jobs.LIFESPAN, job.getLifespan()); - contentValues.put(Jobs.SERIALIZED_DATA, job.getSerializedData()); - contentValues.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0); - - db.insertWithOnConflict(Jobs.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE); - } - - private void insertConstraintSpecs(@NonNull SQLiteDatabase db, @NonNull List constraints) { - for (ConstraintSpec constraintSpec : constraints) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId()); - contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey()); - db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE); - } - } - - private void insertDependencySpecs(@NonNull SQLiteDatabase db, @NonNull List dependencies) { - for (DependencySpec dependencySpec : dependencies) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId()); - contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId()); - db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE); - } - } - - private @NonNull JobSpec jobSpecFromCursor(@NonNull Cursor cursor) { - return new JobSpec(cursor.getString(cursor.getColumnIndexOrThrow(Jobs.JOB_SPEC_ID)), - cursor.getString(cursor.getColumnIndexOrThrow(Jobs.FACTORY_KEY)), - cursor.getString(cursor.getColumnIndexOrThrow(Jobs.QUEUE_KEY)), - cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.CREATE_TIME)), - cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.NEXT_RUN_ATTEMPT_TIME)), - cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.RUN_ATTEMPT)), - cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_ATTEMPTS)), - cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.MAX_BACKOFF)), - cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.LIFESPAN)), - cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)), - cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)), - cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1); - } - - private @NonNull ConstraintSpec constraintSpecFromCursor(@NonNull Cursor cursor) { - return new ConstraintSpec(cursor.getString(cursor.getColumnIndexOrThrow(Constraints.JOB_SPEC_ID)), - cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY))); - } - - private @NonNull DependencySpec dependencySpecFromCursor(@NonNull Cursor cursor) { - return new DependencySpec(cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.JOB_SPEC_ID)), - cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID))); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index b0f6a676c..53f4ea319 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -458,9 +458,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removingIdPrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize())) } - fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) { + fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) { val database = databaseHelper.writableDatabase - val timestamp = Date().time.toString() val index = "$groupPublicKey-$timestamp" val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removingIdPrefixIfNeeded() val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt index 300217fab..1cbbf34c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt @@ -4,11 +4,8 @@ import android.content.ContentValues import android.content.Context import android.database.Cursor import org.session.libsession.messaging.open_groups.OpenGroup -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.JsonUtil import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.dependencies.DatabaseComponent class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { @@ -24,12 +21,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);" } - fun getThreadID(hexEncodedPublicKey: String): Long { - val address = Address.fromSerialized(hexEncodedPublicKey) - val recipient = Recipient.from(context, address, false) - return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) - } - fun getAllOpenGroups(): Map { val database = databaseHelper.readableDatabase var cursor: Cursor? = null @@ -61,6 +52,13 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa } } + fun getThreadId(openGroup: OpenGroup): Long? { + val database = databaseHelper.readableDatabase + return database.get(publicChatTable, "$publicChat = ?", arrayOf(JsonUtil.toJson(openGroup.toJson()))) { cursor -> + cursor.getLong(threadID) + } + } + fun setOpenGroupChat(openGroup: OpenGroup, threadID: Long) { if (threadID < 0) { return diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java index d3ba31747..edc6bc1a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -37,6 +37,13 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn public abstract void markExpireStarted(long messageId, long startTime); public abstract void markAsSent(long messageId, boolean secure); + + public abstract void markAsSyncing(long id); + + public abstract void markAsResyncing(long id); + + public abstract void markAsSyncFailed(long id); + public abstract void markUnidentified(long messageId, boolean unidentified); public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention); @@ -199,7 +206,6 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn contentValues.put(THREAD_ID, newThreadId); db.update(getTableName(), contentValues, where, args); } - public static class SyncMessageId { private final Address address; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index fb815107a..d14e63217 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -20,13 +20,11 @@ import android.content.ContentValues import android.content.Context import android.database.Cursor import com.annimon.stream.Stream -import com.google.android.mms.pdu_alt.NotificationInd import com.google.android.mms.pdu_alt.PduHeaders import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import org.session.libsession.messaging.messages.signal.IncomingMediaMessage -import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage @@ -35,21 +33,19 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.UNKNOWN import org.session.libsession.utilities.Address.Companion.fromExternal import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Contact -import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID import org.session.libsession.utilities.IdentityKeyMismatch import org.session.libsession.utilities.IdentityKeyMismatchList import org.session.libsession.utilities.NetworkFailure import org.session.libsession.utilities.NetworkFailureList import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled import org.session.libsession.utilities.Util.toIsoBytes -import org.session.libsession.utilities.Util.toIsoString import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.recipients.RecipientFormattingException import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils.queue @@ -59,11 +55,13 @@ import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord import org.thoughtcrime.securesms.database.model.Quote import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.mms.MmsException import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.util.asSequence import java.io.Closeable import java.io.IOException import java.security.SecureRandom @@ -90,54 +88,22 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa return 0 } - fun addFailures(messageId: Long, failure: List) { - try { - addToDocument(messageId, NETWORK_FAILURE, failure, NetworkFailureList::class.java) - } catch (e: IOException) { - Log.w(TAG, e) + fun isOutgoingMessage(timestamp: Long): Boolean = + databaseHelper.writableDatabase.query( + TABLE_NAME, + arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS), + DATE_SENT + " = ?", + arrayOf(timestamp.toString()), + null, + null, + null, + null + ).use { cursor -> + cursor.asSequence() + .map { cursor.getColumnIndexOrThrow(MESSAGE_BOX) } + .map(cursor::getLong) + .any { MmsSmsColumns.Types.isOutgoingMessageType(it) } } - } - - fun removeFailure(messageId: Long, failure: NetworkFailure?) { - try { - removeFromDocument(messageId, NETWORK_FAILURE, failure, NetworkFailureList::class.java) - } catch (e: IOException) { - Log.w(TAG, e) - } - } - - fun isOutgoingMessage(timestamp: Long): Boolean { - val database = databaseHelper.writableDatabase - var cursor: Cursor? = null - var isOutgoing = false - try { - cursor = database.query( - TABLE_NAME, - arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS), - DATE_SENT + " = ?", - arrayOf(timestamp.toString()), - null, - null, - null, - null - ) - while (cursor.moveToNext()) { - if (MmsSmsColumns.Types.isOutgoingMessageType( - cursor.getLong( - cursor.getColumnIndexOrThrow( - MESSAGE_BOX - ) - ) - ) - ) { - isOutgoing = true - } - } - } finally { - cursor?.close() - } - return isOutgoing - } fun incrementReceiptCount( messageId: SyncMessageId, @@ -191,7 +157,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ) get(context).groupReceiptDatabase() .update(ourAddress, id, status, timestamp) - get(context).threadDatabase().update(threadId, false) + get(context).threadDatabase().update(threadId, false, true) notifyConversationListeners(threadId) } } @@ -234,34 +200,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } } - @Throws(RecipientFormattingException::class, MmsException::class) - private fun getThreadIdFor(retrieved: IncomingMediaMessage): Long { - return if (retrieved.groupId != null) { - val groupRecipients = Recipient.from( - context, - retrieved.groupId, - true - ) - get(context).threadDatabase().getOrCreateThreadIdFor(groupRecipients) - } else { - val sender = Recipient.from( - context, - retrieved.from, - true - ) - get(context).threadDatabase().getOrCreateThreadIdFor(sender) - } - } - - private fun getThreadIdFor(notification: NotificationInd): Long { - val fromString = - if (notification.from != null && notification.from.textString != null) toIsoString( - notification.from.textString - ) else "" - val recipient = Recipient.from(context, fromExternal(context, fromString), false) - return get(context).threadDatabase().getOrCreateThreadIdFor(recipient) - } - private fun rawQuery(where: String, arguments: Array?): Cursor { val database = databaseHelper.readableDatabase return database.rawQuery( @@ -272,10 +210,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ) } - fun getMessages(idsAsString: String): Cursor { - return rawQuery(idsAsString, null) - } - fun getMessage(messageId: Long): Cursor { val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString())) setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId)) @@ -301,52 +235,44 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa " WHERE " + ID + " = ?", arrayOf(id.toString() + "") ) if (threadId.isPresent) { - get(context).threadDatabase().update(threadId.get(), false) + get(context).threadDatabase().update(threadId.get(), false, true) } } - fun markAsPendingInsecureSmsFallback(messageId: Long) { - val threadId = getThreadIdForMessage(messageId) + private fun markAs( + messageId: Long, + baseType: Long, + threadId: Long = getThreadIdForMessage(messageId) + ) { updateMailboxBitmask( messageId, MmsSmsColumns.Types.BASE_TYPE_MASK, - MmsSmsColumns.Types.BASE_PENDING_INSECURE_SMS_FALLBACK, + baseType, Optional.of(threadId) ) notifyConversationListeners(threadId) } + override fun markAsSyncing(messageId: Long) { + markAs(messageId, MmsSmsColumns.Types.BASE_SYNCING_TYPE) + } + override fun markAsResyncing(messageId: Long) { + markAs(messageId, MmsSmsColumns.Types.BASE_RESYNCING_TYPE) + } + override fun markAsSyncFailed(messageId: Long) { + markAs(messageId, MmsSmsColumns.Types.BASE_SYNC_FAILED_TYPE) + } + fun markAsSending(messageId: Long) { - val threadId = getThreadIdForMessage(messageId) - updateMailboxBitmask( - messageId, - MmsSmsColumns.Types.BASE_TYPE_MASK, - MmsSmsColumns.Types.BASE_SENDING_TYPE, - Optional.of(threadId) - ) - notifyConversationListeners(threadId) + markAs(messageId, MmsSmsColumns.Types.BASE_SENDING_TYPE) } fun markAsSentFailed(messageId: Long) { - val threadId = getThreadIdForMessage(messageId) - updateMailboxBitmask( - messageId, - MmsSmsColumns.Types.BASE_TYPE_MASK, - MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE, - Optional.of(threadId) - ) - notifyConversationListeners(threadId) + markAs(messageId, MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE) } override fun markAsSent(messageId: Long, secure: Boolean) { - val threadId = getThreadIdForMessage(messageId) - updateMailboxBitmask( - messageId, - MmsSmsColumns.Types.BASE_TYPE_MASK, - MmsSmsColumns.Types.BASE_SENT_TYPE or if (secure) MmsSmsColumns.Types.PUSH_MESSAGE_BIT or MmsSmsColumns.Types.SECURE_MESSAGE_BIT else 0, - Optional.of(threadId) - ) - notifyConversationListeners(threadId) + markAs(messageId, MmsSmsColumns.Types.BASE_SENT_TYPE or if (secure) MmsSmsColumns.Types.PUSH_MESSAGE_BIT or MmsSmsColumns.Types.SECURE_MESSAGE_BIT else 0) } override fun markUnidentified(messageId: Long, unidentified: Boolean) { @@ -366,21 +292,12 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val attachmentDatabase = get(context).attachmentDatabase() queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) }) val threadId = getThreadIdForMessage(messageId) - if (!read) { - val mentionChange = if (hasMention) { 1 } else { 0 } - get(context).threadDatabase().decrementUnread(threadId, 1, mentionChange) - } - updateMailboxBitmask( - messageId, - MmsSmsColumns.Types.BASE_TYPE_MASK, - MmsSmsColumns.Types.BASE_DELETED_TYPE, - Optional.of(threadId) - ) - notifyConversationListeners(threadId) + + markAs(messageId, MmsSmsColumns.Types.BASE_DELETED_TYPE, threadId) } override fun markExpireStarted(messageId: Long) { - markExpireStarted(messageId, System.currentTimeMillis()) + markExpireStarted(messageId, SnodeAPI.nowWithOffset) } override fun markExpireStarted(messageId: Long, startedTimestamp: Long) { @@ -399,6 +316,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(id.toString())) } + fun setMessagesRead(threadId: Long, beforeTime: Long): List { + return setMessagesRead( + THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?", + arrayOf(threadId.toString(), beforeTime.toString()) + ) + } + fun setMessagesRead(threadId: Long): List { return setMessagesRead( THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", @@ -406,10 +330,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ) } - fun setAllMessagesRead(): List { - return setMessagesRead(READ + " = 0", null) - } - private fun setMessagesRead(where: String, arguments: Array?): List { val database = databaseHelper.writableDatabase val result: MutableList = LinkedList() @@ -418,7 +338,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa try { cursor = database.query( TABLE_NAME, - arrayOf(ID, ADDRESS, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED), + arrayOf(ID, ADDRESS, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED), where, arguments, null, @@ -627,18 +547,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa contentLocation: String, threadId: Long, mailbox: Long, serverTimestamp: Long, - runIncrement: Boolean, runThreadUpdate: Boolean ): Optional { - var threadId = threadId - if (threadId == -1L || retrieved.isGroupMessage) { - try { - threadId = getThreadIdFor(retrieved) - } catch (e: RecipientFormattingException) { - Log.w("MmsDatabase", e) - if (threadId == -1L) throw MmsException(e) - } - } + if (threadId < 0 ) throw MmsException("No thread ID supplied!") val contentValues = ContentValues() contentValues.put(DATE_SENT, retrieved.sentTimeMillis) contentValues.put(ADDRESS, retrieved.from.serialize()) @@ -692,12 +603,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa null, ) if (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) { - if (runIncrement) { - val mentionAmount = if (retrieved.hasMention()) { 1 } else { 0 } - get(context).threadDatabase().incrementUnread(threadId, 1, mentionAmount) - } if (runThreadUpdate) { - get(context).threadDatabase().update(threadId, true) + get(context).threadDatabase().update(threadId, true, true) } } notifyConversationListeners(threadId) @@ -711,27 +618,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa serverTimestamp: Long, runThreadUpdate: Boolean ): Optional { - var threadId = threadId - if (threadId == -1L) { - if (retrieved.isGroup) { - val decodedGroupId: String = if (retrieved is OutgoingExpirationUpdateMessage) { - retrieved.groupId - } else { - (retrieved as OutgoingGroupMediaMessage).groupId - } - val groupId: String - groupId = try { - doubleEncodeGroupID(decodedGroupId) - } catch (e: IOException) { - Log.e(TAG, "Couldn't encrypt group ID") - throw MmsException(e) - } - val group = Recipient.from(context, fromSerialized(groupId), false) - threadId = get(context).threadDatabase().getOrCreateThreadIdFor(group) - } else { - threadId = get(context).threadDatabase().getOrCreateThreadIdFor(retrieved.recipient) - } - } + if (threadId < 0 ) throw MmsException("No thread ID supplied!") val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate) if (messageId == -1L) { return Optional.absent() @@ -746,7 +633,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa retrieved: IncomingMediaMessage, threadId: Long, serverTimestamp: Long = 0, - runIncrement: Boolean, runThreadUpdate: Boolean ): Optional { var type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT @@ -765,7 +651,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa if (retrieved.isMessageRequestResponse) { type = type or MmsSmsColumns.Types.MESSAGE_REQUEST_RESPONSE_BIT } - return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runIncrement, runThreadUpdate) + return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runThreadUpdate) } @JvmOverloads @@ -798,7 +684,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa // In open groups messages should be sorted by their server timestamp var receivedTimestamp = serverTimestamp if (serverTimestamp == 0L) { - receivedTimestamp = System.currentTimeMillis() + receivedTimestamp = SnodeAPI.nowWithOffset } contentValues.put(DATE_RECEIVED, receivedTimestamp) contentValues.put(SUBSCRIPTION_ID, message.subscriptionId) @@ -854,10 +740,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ) } with (get(context).threadDatabase()) { - setLastSeen(threadId) + val lastSeen = getLastSeenAndHasSent(threadId).first() + if (lastSeen < message.sentTimeMillis) { + setLastSeen(threadId, message.sentTimeMillis) + } setHasSent(threadId, true) if (runThreadUpdate) { - update(threadId, true) + update(threadId, true, true) } } return messageId @@ -975,7 +864,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa groupReceiptDatabase.deleteRowsForMessage(messageId) val database = databaseHelper.writableDatabase database!!.delete(TABLE_NAME, ID_WHERE, arrayOf(messageId.toString())) - val threadDeleted = get(context).threadDatabase().update(threadId, false) + val threadDeleted = get(context).threadDatabase().update(threadId, false, true) notifyConversationListeners(threadId) notifyStickerListeners() notifyStickerPackListeners() @@ -992,7 +881,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val database = databaseHelper.writableDatabase database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(","))) - val threadDeleted = get(context).threadDatabase().update(threadId, false) + val threadDeleted = get(context).threadDatabase().update(threadId, false, true) notifyConversationListeners(threadId) notifyStickerListeners() notifyStickerPackListeners() @@ -1245,7 +1134,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } val threadDb = get(context).threadDatabase() for (threadId in threadIds) { - val threadDeleted = threadDb.update(threadId, false) + val threadDeleted = threadDb.update(threadId, false, true) notifyConversationListeners(threadId) } notifyStickerListeners() @@ -1315,7 +1204,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val slideDeck = SlideDeck(context, message!!.attachments) return MediaMmsMessageRecord( id, message.recipient, message.recipient, - 1, System.currentTimeMillis(), System.currentTimeMillis(), + 1, SnodeAPI.nowWithOffset, SnodeAPI.nowWithOffset, 0, threadId, message.body, slideDeck, slideDeck.slides.size, if (message.isSecure) MmsSmsColumns.Types.getOutgoingEncryptedMessageType() else MmsSmsColumns.Types.getOutgoingSmsMessageType(), @@ -1323,7 +1212,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa LinkedList(), message.subscriptionId, message.expiresIn, - System.currentTimeMillis(), 0, + SnodeAPI.nowWithOffset, 0, if (message.outgoingQuote != null) Quote( message.outgoingQuote!!.id, message.outgoingQuote!!.author, @@ -1437,25 +1326,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val attachments = get(context).attachmentDatabase().getAttachment( cursor ) - val contacts: List = getSharedContacts( - cursor, attachments - ) - val contactAttachments = - contacts.map { obj: Contact? -> obj!!.avatarAttachment } - .filter { a: Attachment? -> a != null } - .toSet() - val previews: List = getLinkPreviews( - cursor, attachments - ) - val previewAttachments = - previews.filter { lp: LinkPreview? -> lp!!.getThumbnail().isPresent } - .map { lp: LinkPreview? -> lp!!.getThumbnail().get() } - .toSet() + val contacts: List = getSharedContacts(cursor, attachments) + val contactAttachments: Set = + contacts.mapNotNull { it?.avatarAttachment }.toSet() + val previews: List = getLinkPreviews(cursor, attachments) + val previewAttachments: Set = + previews.mapNotNull { it?.getThumbnail()?.orNull() }.toSet() val slideDeck = getSlideDeck( - Stream.of(attachments) - .filterNot { o: DatabaseAttachment? -> contactAttachments.contains(o) } - .filterNot { o: DatabaseAttachment? -> previewAttachments.contains(o) } - .toList() + attachments + .filterNot { o: DatabaseAttachment? -> o in contactAttachments } + .filterNot { o: DatabaseAttachment? -> o in previewAttachments } ) val quote = getQuote(cursor) val reactions = get(context).reactionDatabase().getReactions(cursor) @@ -1513,11 +1393,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor) val quoteText = retrievedQuote?.body val quoteMissing = retrievedQuote == null - val attachments = get(context).attachmentDatabase().getAttachment(cursor) - val quoteAttachments: List? = - Stream.of(attachments).filter { obj: DatabaseAttachment? -> obj!!.isQuote } + val quoteDeck = ( + (retrievedQuote as? MmsMessageRecord)?.slideDeck ?: + Stream.of(get(context).attachmentDatabase().getAttachment(cursor)) + .filter { obj: DatabaseAttachment? -> obj!!.isQuote } .toList() - val quoteDeck = SlideDeck(context, quoteAttachments!!) + .let { SlideDeck(context, it) } + ) return Quote( quoteId, fromExternal(context, quoteAuthor), @@ -1617,6 +1499,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, + HAS_MENTION, "json_group_array(json_object(" + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + @@ -1659,4 +1542,4 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa const val CREATE_REACTIONS_LAST_SEEN_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $REACTIONS_LAST_SEEN INTEGER DEFAULT 0;" const val CREATE_HAS_MENTION_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $HAS_MENTION INTEGER DEFAULT 0;" } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index f3110a5c7..1e1cc5089 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -47,8 +47,13 @@ public interface MmsSmsColumns { protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26; public static final long BASE_DRAFT_TYPE = 27; protected static final long BASE_DELETED_TYPE = 28; + protected static final long BASE_SYNCING_TYPE = 29; + protected static final long BASE_RESYNCING_TYPE = 30; + protected static final long BASE_SYNC_FAILED_TYPE = 31; protected static final long[] OUTGOING_MESSAGE_TYPES = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE, + BASE_SYNCING_TYPE, BASE_RESYNCING_TYPE, + BASE_SYNC_FAILED_TYPE, BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE, BASE_PENDING_SECURE_SMS_FALLBACK, BASE_PENDING_INSECURE_SMS_FALLBACK, @@ -109,6 +114,18 @@ public interface MmsSmsColumns { return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE; } + public static boolean isResyncingType(long type) { + return (type & BASE_TYPE_MASK) == BASE_RESYNCING_TYPE; + } + + public static boolean isSyncingType(long type) { + return (type & BASE_TYPE_MASK) == BASE_SYNCING_TYPE; + } + + public static boolean isSyncFailedMessageType(long type) { + return (type & BASE_TYPE_MASK) == BASE_SYNC_FAILED_TYPE; + } + public static boolean isFailedMessageType(long type) { return (type & BASE_TYPE_MASK) == BASE_SENT_FAILED_TYPE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index c7f9d6132..0db4dd00e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -16,6 +16,8 @@ */ package org.thoughtcrime.securesms.database; +import static org.thoughtcrime.securesms.database.MmsDatabase.MESSAGE_BOX; + import android.content.Context; import android.database.Cursor; @@ -25,6 +27,7 @@ import androidx.annotation.Nullable; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteQueryBuilder; +import org.jetbrains.annotations.NotNull; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Util; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; @@ -36,6 +39,8 @@ import java.io.Closeable; import java.util.HashSet; import java.util.Set; +import kotlin.Pair; + public class MmsSmsDatabase extends Database { @SuppressWarnings("unused") @@ -259,8 +264,8 @@ public class MmsSmsDatabase extends Database { return -1; } - public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address) { - String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; + public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address, boolean reverse) { + String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC"); String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) { @@ -512,6 +517,23 @@ public class MmsSmsDatabase extends Database { return new Reader(cursor); } + @NotNull + public Pair timestampAndDirectionForCurrent(@NotNull Cursor cursor) { + int sentColumn = cursor.getColumnIndex(MmsSmsColumns.NORMALIZED_DATE_SENT); + String msgType = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT)); + long sentTime = cursor.getLong(sentColumn); + long type = 0; + if (MmsSmsDatabase.MMS_TRANSPORT.equals(msgType)) { + int typeIndex = cursor.getColumnIndex(MESSAGE_BOX); + type = cursor.getLong(typeIndex); + } else if (MmsSmsDatabase.SMS_TRANSPORT.equals(msgType)) { + int typeIndex = cursor.getColumnIndex(SmsDatabase.TYPE); + type = cursor.getLong(typeIndex); + } + + return new Pair(MmsSmsColumns.Types.isOutgoingMessageType(type), sentTime); + } + public class Reader implements Closeable { private final Cursor cursor; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index ab4cb9f2e..28fe60897 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -62,6 +62,7 @@ public class RecipientDatabase extends Database { private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode"; private static final String FORCE_SMS_SELECTION = "force_sms_selection"; private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none + private static final String WRAPPER_HASH = "wrapper_hash"; private static final String AUTO_DOWNLOAD = "auto_download"; // 1 / 0 / -1 flag for whether to auto-download in a conversation, or if the user hasn't selected a preference private static final String[] RECIPIENT_PROJECTION = new String[] { @@ -69,7 +70,7 @@ public class RecipientDatabase extends Database { PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI, SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL, UNIDENTIFIED_ACCESS_MODE, - FORCE_SMS_SELECTION, NOTIFY_TYPE, AUTO_DOWNLOAD, + FORCE_SMS_SELECTION, NOTIFY_TYPE, WRAPPER_HASH, AUTO_DOWNLOAD, }; static final List TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) @@ -148,6 +149,11 @@ public class RecipientDatabase extends Database { "OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))"; } + public static String getAddWrapperHash() { + return "ALTER TABLE "+TABLE_NAME+" "+ + "ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;"; + } + public static final int NOTIFY_TYPE_ALL = 0; public static final int NOTIFY_TYPE_MENTIONS = 1; public static final int NOTIFY_TYPE_NONE = 2; @@ -166,18 +172,14 @@ public class RecipientDatabase extends Database { public Optional getRecipientSettings(@NonNull Address address) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = null; - try { - cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[] {address.serialize()}, null, null, null); + try (Cursor cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) { if (cursor != null && cursor.moveToNext()) { return getRecipientSettings(cursor); } return Optional.absent(); - } finally { - if (cursor != null) cursor.close(); } } @@ -207,6 +209,7 @@ public class RecipientDatabase extends Database { String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL)); int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE)); boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1; + String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH)); MaterialColor color; byte[] profileKey = null; @@ -238,7 +241,7 @@ public class RecipientDatabase extends Database { systemPhoneLabel, systemContactUri, signalProfileName, signalProfileAvatar, profileSharing, notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode), - forceSmsSelection)); + forceSmsSelection, wrapperHash)); } public boolean isAutoDownloadFlagSet(Recipient recipient) { @@ -281,6 +284,24 @@ public class RecipientDatabase extends Database { notifyRecipientListeners(); } + public boolean getApproved(@NonNull Address address) { + SQLiteDatabase db = getReadableDatabase(); + try (Cursor cursor = db.query(TABLE_NAME, new String[]{APPROVED}, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) { + if (cursor != null && cursor.moveToNext()) { + return cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED)) == 1; + } + } + return false; + } + + public void setRecipientHash(@NonNull Recipient recipient, String recipientHash) { + ContentValues values = new ContentValues(); + values.put(WRAPPER_HASH, recipientHash); + updateOrInsert(recipient.getAddress(), values); + recipient.resolve().setWrapperHash(recipientHash); + notifyRecipientListeners(); + } + public void setApproved(@NonNull Recipient recipient, boolean approved) { ContentValues values = new ContentValues(); values.put(APPROVED, approved ? 1 : 0); @@ -297,15 +318,7 @@ public class RecipientDatabase extends Database { notifyRecipientListeners(); } - public void setBlocked(@NonNull Recipient recipient, boolean blocked) { - ContentValues values = new ContentValues(); - values.put(BLOCK, blocked ? 1 : 0); - updateOrInsert(recipient.getAddress(), values); - recipient.resolve().setBlocked(blocked); - notifyRecipientListeners(); - } - - public void setBlocked(@NonNull List recipients, boolean blocked) { + public void setBlocked(@NonNull Iterable recipients, boolean blocked) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt index 7080d9cb8..51365bb04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt @@ -3,8 +3,11 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import android.database.Cursor +import androidx.core.database.getStringOrNull import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.messaging.utilities.SessionId import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { @@ -42,6 +45,9 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da val database = databaseHelper.readableDatabase return database.getAll(sessionContactTable, null, null) { cursor -> contactFromCursor(cursor) + }.filter { contact -> + val sessionId = SessionId(contact.sessionID) + sessionId.prefix == IdPrefix.STANDARD }.toSet() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt index 4425e3d85..6221446aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt @@ -46,7 +46,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa databaseHelper.writableDatabase.delete(sessionJobTable, "${Companion.jobID} = ?", arrayOf( jobID )) } - fun getAllPendingJobs(type: String): Map { + fun getAllJobs(type: String): Map { val database = databaseHelper.readableDatabase return database.getAll(sessionJobTable, "$jobType = ?", arrayOf( type )) { cursor -> val jobID = cursor.getString(jobID) @@ -83,16 +83,17 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa } } - fun getGroupAvatarDownloadJob(server: String, room: String): GroupAvatarDownloadJob? { + fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): GroupAvatarDownloadJob? { val database = databaseHelper.readableDatabase return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(GroupAvatarDownloadJob.KEY)) { jobFromCursor(it) as GroupAvatarDownloadJob? - }.filterNotNull().find { it.server == server && it.room == room } + }.filterNotNull().find { it.server == server && it.room == room && (imageId == null || it.imageId == imageId) } } fun cancelPendingMessageSendJobs(threadID: Long) { val database = databaseHelper.writableDatabase val attachmentUploadJobKeys = mutableListOf() + database.beginTransaction() database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor -> val job = jobFromCursor(cursor) as AttachmentUploadJob? if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) } @@ -103,15 +104,19 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) } } if (attachmentUploadJobKeys.isNotEmpty()) { - val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ") - database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)", - arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString )) + attachmentUploadJobKeys.forEach { + database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} = ?", + arrayOf( AttachmentUploadJob.KEY, it )) + } } if (messageSendJobKeys.isNotEmpty()) { - val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ") - database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)", - arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString )) + messageSendJobKeys.forEach { + database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} = ?", + arrayOf( MessageSendJob.KEY, it )) + } } + database.setTransactionSuccessful() + database.endTransaction() } fun isJobCanceled(job: Job): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 76c3e6b9c..e48f27d49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -36,6 +36,7 @@ import org.session.libsession.messaging.calls.CallMessageType; import org.session.libsession.messaging.messages.signal.IncomingGroupMessage; import org.session.libsession.messaging.messages.signal.IncomingTextMessage; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.IdentityKeyMismatchList; @@ -105,7 +106,7 @@ public class SmsDatabase extends MessagingDatabase { PROTOCOL, READ, STATUS, TYPE, REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT, MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED, - NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, + NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, HAS_MENTION, "json_group_array(json_object(" + "'" + ReactionDatabase.ROW_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.ROW_ID + ", " + "'" + ReactionDatabase.MESSAGE_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + ", " + @@ -147,7 +148,7 @@ public class SmsDatabase extends MessagingDatabase { long threadId = getThreadIdForMessage(id); - DatabaseComponent.get(context).threadDatabase().update(threadId, false); + DatabaseComponent.get(context).threadDatabase().update(threadId, false, true); notifyConversationListeners(threadId); } @@ -201,6 +202,21 @@ public class SmsDatabase extends MessagingDatabase { updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE); } + @Override + public void markAsSyncing(long id) { + updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SYNCING_TYPE); + } + + @Override + public void markAsResyncing(long id) { + updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_RESYNCING_TYPE); + } + + @Override + public void markAsSyncFailed(long id) { + updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SYNC_FAILED_TYPE); + } + @Override public void markUnidentified(long id, boolean unidentified) { ContentValues contentValues = new ContentValues(1); @@ -218,16 +234,12 @@ public class SmsDatabase extends MessagingDatabase { contentValues.put(BODY, ""); contentValues.put(HAS_MENTION, 0); database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); - long threadId = getThreadIdForMessage(messageId); - if (!read) { - DatabaseComponent.get(context).threadDatabase().decrementUnread(threadId, 1, (hasMention ? 1 : 0)); - } updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE); } @Override public void markExpireStarted(long id) { - markExpireStarted(id, System.currentTimeMillis()); + markExpireStarted(id, SnodeAPI.getNowWithOffset()); } @Override @@ -240,7 +252,7 @@ public class SmsDatabase extends MessagingDatabase { long threadId = getThreadIdForMessage(id); - DatabaseComponent.get(context).threadDatabase().update(threadId, false); + DatabaseComponent.get(context).threadDatabase().update(threadId, false, true); notifyConversationListeners(threadId); } @@ -303,7 +315,7 @@ public class SmsDatabase extends MessagingDatabase { ID + " = ?", new String[] {String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))}); - DatabaseComponent.get(context).threadDatabase().update(threadId, false); + DatabaseComponent.get(context).threadDatabase().update(threadId, false, true); notifyConversationListeners(threadId); foundMessage = true; } @@ -321,6 +333,9 @@ public class SmsDatabase extends MessagingDatabase { } } + public List setMessagesRead(long threadId, long beforeTime) { + return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?", new String[]{threadId+"", beforeTime+""}); + } public List setMessagesRead(long threadId) { return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", new String[] {String.valueOf(threadId)}); } @@ -384,14 +399,14 @@ public class SmsDatabase extends MessagingDatabase { long threadId = getThreadIdForMessage(messageId); - DatabaseComponent.get(context).threadDatabase().update(threadId, true); + DatabaseComponent.get(context).threadDatabase().update(threadId, true, true); notifyConversationListeners(threadId); notifyConversationListListeners(); return new Pair<>(messageId, threadId); } - protected Optional insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) { + protected Optional insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runThreadUpdate) { if (message.isSecureMessage()) { type |= Types.SECURE_MESSAGE_BIT; } else if (message.isGroup()) { @@ -470,12 +485,8 @@ public class SmsDatabase extends MessagingDatabase { SQLiteDatabase db = databaseHelper.getWritableDatabase(); long messageId = db.insert(TABLE_NAME, null, values); - if (unread && runIncrement) { - DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1, (message.hasMention() ? 1 : 0)); - } - if (runThreadUpdate) { - DatabaseComponent.get(context).threadDatabase().update(threadId, true); + DatabaseComponent.get(context).threadDatabase().update(threadId, true, true); } if (message.getSubscriptionId() != -1) { @@ -488,16 +499,16 @@ public class SmsDatabase extends MessagingDatabase { } } - public Optional insertMessageInbox(IncomingTextMessage message, boolean runIncrement, boolean runThreadUpdate) { - return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runIncrement, runThreadUpdate); + public Optional insertMessageInbox(IncomingTextMessage message, boolean runThreadUpdate) { + return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runThreadUpdate); } public Optional insertCallMessage(IncomingTextMessage message) { - return insertMessageInbox(message, 0, 0, true, true); + return insertMessageInbox(message, 0, 0, true); } - public Optional insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) { - return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runIncrement, runThreadUpdate); + public Optional insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runThreadUpdate) { + return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runThreadUpdate); } public Optional insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp, boolean runThreadUpdate) { @@ -530,7 +541,7 @@ public class SmsDatabase extends MessagingDatabase { contentValues.put(ADDRESS, address.serialize()); contentValues.put(THREAD_ID, threadId); contentValues.put(BODY, message.getMessageBody()); - contentValues.put(DATE_RECEIVED, System.currentTimeMillis()); + contentValues.put(DATE_RECEIVED, SnodeAPI.getNowWithOffset()); contentValues.put(DATE_SENT, message.getSentTimestampMillis()); contentValues.put(READ, 1); contentValues.put(TYPE, type); @@ -551,9 +562,12 @@ public class SmsDatabase extends MessagingDatabase { } if (runThreadUpdate) { - DatabaseComponent.get(context).threadDatabase().update(threadId, true); + DatabaseComponent.get(context).threadDatabase().update(threadId, true, true); + } + long lastSeen = DatabaseComponent.get(context).threadDatabase().getLastSeenAndHasSent(threadId).first(); + if (lastSeen < message.getSentTimestampMillis()) { + DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId, message.getSentTimestampMillis()); } - DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId); DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true); @@ -600,7 +614,7 @@ public class SmsDatabase extends MessagingDatabase { SQLiteDatabase db = databaseHelper.getWritableDatabase(); long threadId = getThreadIdForMessage(messageId); db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); - boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false); + boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true); notifyConversationListeners(threadId); return threadDeleted; } @@ -624,7 +638,7 @@ public class SmsDatabase extends MessagingDatabase { ID + " IN (" + StringUtils.join(argsArray, ',') + ")", argValues ); - boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false); + boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true); notifyConversationListeners(threadId); return threadDeleted; } @@ -770,11 +784,11 @@ public class SmsDatabase extends MessagingDatabase { public MessageRecord getCurrent() { return new SmsMessageRecord(id, message.getMessageBody(), message.getRecipient(), message.getRecipient(), - System.currentTimeMillis(), System.currentTimeMillis(), + SnodeAPI.getNowWithOffset(), SnodeAPI.getNowWithOffset(), 0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), threadId, 0, new LinkedList(), message.getExpiresIn(), - System.currentTimeMillis(), 0, false, Collections.emptyList(), false); + SnodeAPI.getNowWithOffset(), 0, false, Collections.emptyList(), false); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index e43102086..f42cfc00e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -2,16 +2,43 @@ package org.thoughtcrime.securesms.database import android.content.Context import android.net.Uri +import network.loki.messenger.libsession_util.ConfigBase +import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN +import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED +import network.loki.messenger.libsession_util.Contacts +import network.loki.messenger.libsession_util.ConversationVolatileConfig +import network.loki.messenger.libsession_util.UserGroupsConfig +import network.loki.messenger.libsession_util.UserProfile +import network.loki.messenger.libsession_util.util.BaseCommunityInfo +import network.loki.messenger.libsession_util.util.Conversation +import network.loki.messenger.libsession_util.util.ExpiryMode +import network.loki.messenger.libsession_util.util.GroupInfo +import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.avatars.AvatarHelper import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.messaging.jobs.* +import org.session.libsession.messaging.jobs.AttachmentUploadJob +import org.session.libsession.messaging.jobs.BackgroundGroupAddJob +import org.session.libsession.messaging.jobs.ConfigurationSyncJob +import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob +import org.session.libsession.messaging.jobs.Job +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveJob +import org.session.libsession.messaging.jobs.MessageSendJob +import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob +import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.MessageRequestResponse -import org.session.libsession.messaging.messages.signal.* +import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage +import org.session.libsession.messaging.messages.signal.IncomingGroupMessage +import org.session.libsession.messaging.messages.signal.IncomingMediaMessage +import org.session.libsession.messaging.messages.signal.IncomingTextMessage +import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Profile import org.session.libsession.messaging.messages.visible.Reaction @@ -23,12 +50,15 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview +import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 +import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.utilities.* +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.GroupRecord import org.session.libsession.utilities.GroupUtil @@ -36,25 +66,104 @@ import org.session.libsession.utilities.ProfileKeyUtil import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.crypto.ecc.DjbECPrivateKey +import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.KeyHelper +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.guava.Optional -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.groups.ClosedGroupManager +import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager -import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.SessionMetaProtocol import java.security.MessageDigest +import network.loki.messenger.libsession_util.util.Contact as LibSessionContact + +open class Storage(context: Context, helper: SQLCipherOpenHelper, private val configFactory: ConfigFactory) : Database(context, helper), StorageProtocol, + ThreadDatabase.ConversationThreadUpdateListener { + + override fun threadCreated(address: Address, threadId: Long) { + val localUserAddress = getUserPublicKey() ?: return + if (!getRecipientApproved(address) && localUserAddress != address.serialize()) return // don't store unapproved / message requests + + val volatile = configFactory.convoVolatile ?: return + if (address.isGroup) { + val groups = configFactory.userGroups ?: return + if (address.isClosedGroup) { + val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize()) + val closedGroup = getGroup(address.toGroupString()) + if (closedGroup != null && closedGroup.isActive) { + val legacyGroup = groups.getOrConstructLegacyGroupInfo(sessionId) + groups.set(legacyGroup) + val newVolatileParams = volatile.getOrConstructLegacyGroup(sessionId).copy( + lastRead = SnodeAPI.nowWithOffset, + ) + volatile.set(newVolatileParams) + } + } else if (address.isOpenGroup) { + // these should be added on the group join / group info fetch + Log.w("Loki", "Thread created called for open group address, not adding any extra information") + } + } else if (address.isContact) { + // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config + if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return + // don't update our own address into the contacts DB + if (getUserPublicKey() != address.serialize()) { + val contacts = configFactory.contacts ?: return + contacts.upsertContact(address.serialize()) { + priority = ConfigBase.PRIORITY_VISIBLE + } + } else { + val userProfile = configFactory.user ?: return + userProfile.setNtsPriority(ConfigBase.PRIORITY_VISIBLE) + DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true) + } + val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize()) + volatile.set(newVolatileParams) + } + } + + override fun threadDeleted(address: Address, threadId: Long) { + val volatile = configFactory.convoVolatile ?: return + if (address.isGroup) { + val groups = configFactory.userGroups ?: return + if (address.isClosedGroup) { + val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize()) + volatile.eraseLegacyClosedGroup(sessionId) + groups.eraseLegacyGroup(sessionId) + } else if (address.isOpenGroup) { + // these should be removed in the group leave / handling new configs + Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere") + } + } else { + // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config + if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return + volatile.eraseOneToOne(address.serialize()) + if (getUserPublicKey() != address.serialize()) { + val contacts = configFactory.contacts ?: return + contacts.upsertContact(address.serialize()) { + priority = PRIORITY_HIDDEN + } + } else { + val userProfile = configFactory.user ?: return + userProfile.setNtsPriority(PRIORITY_HIDDEN) + } + } + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } -class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol { - override fun getUserPublicKey(): String? { return TextSecurePreferences.getLocalNumber(context) } @@ -70,13 +179,28 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return Profile(displayName, profileKey, profilePictureUrl) } - override fun setUserProfilePictureURL(newValue: String) { + override fun setProfileAvatar(recipient: Recipient, profileAvatar: String?) { + val database = DatabaseComponent.get(context).recipientDatabase() + database.setProfileAvatar(recipient, profileAvatar) + } + + override fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) { + val db = DatabaseComponent.get(context).recipientDatabase() + db.setProfileAvatar(recipient, newProfilePicture) + db.setProfileKey(recipient, newProfileKey) + } + + override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) { val ourRecipient = fromSerialized(getUserPublicKey()!!).let { Recipient.from(context, it, false) } - TextSecurePreferences.setProfilePictureURL(context, newValue) - RetrieveProfileAvatarJob(ourRecipient, newValue) - ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newValue)) + ourRecipient.resolve().profileKey = newProfileKey + TextSecurePreferences.setProfileKey(context, newProfileKey?.let { Base64.encodeBytes(it) }) + TextSecurePreferences.setProfilePictureURL(context, newProfilePicture) + + if (newProfileKey != null) { + JobQueue.shared.add(RetrieveProfileAvatarJob(newProfilePicture, ourRecipient.address)) + } } override fun getOrGenerateRegistrationID(): Int { @@ -99,19 +223,56 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return database.getAttachmentsForMessage(messageID) } - override fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) { + override fun getLastSeen(threadId: Long): Long { val threadDb = DatabaseComponent.get(context).threadDatabase() - threadDb.setRead(threadId, updateLastSeen) + return threadDb.getLastSeenAndHasSent(threadId)?.first() ?: 0L } - override fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int) { + override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean) { val threadDb = DatabaseComponent.get(context).threadDatabase() - threadDb.incrementUnread(threadId, amount, unreadMentionAmount) + getRecipientForThread(threadId)?.let { recipient -> + val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first() + // don't set the last read in the volatile if we didn't set it in the DB + if (!threadDb.markAllAsRead(threadId, recipient.isGroupRecipient, lastSeenTime, force) && !force) return + + // don't process configs for inbox recipients + if (recipient.isOpenGroupInboxRecipient) return + + configFactory.convoVolatile?.let { config -> + val convo = when { + // recipient closed group + recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize())) + // recipient is open group + recipient.isOpenGroupRecipient -> { + val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return + BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) -> + config.getOrConstructCommunity(base, room, pubKey) + } ?: return + } + // otherwise recipient is one to one + recipient.isContactRecipient -> { + // don't process non-standard session IDs though + val sessionId = SessionId(recipient.address.serialize()) + if (sessionId.prefix != IdPrefix.STANDARD) return + + config.getOrConstructOneToOne(recipient.address.serialize()) + } + else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}") + } + convo.lastRead = lastSeenTime + if (convo.unread) { + convo.unread = lastSeenTime <= currentLastRead + notifyConversationListListeners() + } + config.set(convo) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } + } } override fun updateThread(threadId: Long, unarchive: Boolean) { val threadDb = DatabaseComponent.get(context).threadDatabase() - threadDb.update(threadId, unarchive) + threadDb.update(threadId, unarchive, false) } override fun persist(message: VisibleMessage, @@ -120,7 +281,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, groupPublicKey: String?, openGroupID: String?, attachments: List, - runIncrement: Boolean, runThreadUpdate: Boolean): Long? { var messageID: Long? = null val senderAddress = fromSerialized(message.sender!!) @@ -147,13 +307,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } val targetRecipient = Recipient.from(context, targetAddress, false) if (!targetRecipient.isGroupRecipient) { - val recipientDb = DatabaseComponent.get(context).recipientDatabase() if (isUserSender || isUserBlindedSender) { - recipientDb.setApproved(targetRecipient, true) + setRecipientApproved(targetRecipient, true) } else { - recipientDb.setApprovedMe(targetRecipient, true) + setRecipientApprovedMe(targetRecipient, true) } } + if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) { + // open group recipients should explicitly create threads + message.threadID = getOrCreateThreadIdFor(targetAddress) + } if (message.isMediaMessage() || attachments.isNotEmpty()) { val quote: Optional = if (quotes != null) Optional.of(quotes) else Optional.absent() val linkPreviews: Optional> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! }) @@ -167,7 +330,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, it.toSignalPointer() } val mediaMessage = IncomingMediaMessage.from(message, senderAddress, targetRecipient.expireMessages * 1000L, group, signalServiceAttachments, quote, linkPreviews) - mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate) + mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID!!, message.receivedTimestamp ?: 0, runThreadUpdate) } if (insertResult.isPresent) { messageID = insertResult.get().messageId @@ -184,7 +347,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp) else IncomingTextMessage.from(message, senderAddress, group, targetRecipient.expireMessages * 1000L) val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody) - smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate) + smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runThreadUpdate) } insertResult.orNull()?.let { result -> messageID = result.messageId @@ -211,7 +374,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } override fun getAllPendingJobs(type: String): Map { - return DatabaseComponent.get(context).sessionJobDatabase().getAllPendingJobs(type) + return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(type) } override fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? { @@ -226,8 +389,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseComponent.get(context).sessionJobDatabase().getMessageReceiveJob(messageReceiveJobID) } - override fun getGroupAvatarDownloadJob(server: String, room: String): GroupAvatarDownloadJob? { - return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room) + override fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): GroupAvatarDownloadJob? { + return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room, imageId) + } + + override fun getConfigSyncJob(destination: Destination): Job? { + return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(ConfigurationSyncJob.KEY).values.firstOrNull { + (it as? ConfigurationSyncJob)?.destination == destination + } } override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) { @@ -239,11 +408,201 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseComponent.get(context).sessionJobDatabase().isJobCanceled(job) } + override fun cancelPendingMessageSendJobs(threadID: Long) { + val jobDb = DatabaseComponent.get(context).sessionJobDatabase() + jobDb.cancelPendingMessageSendJobs(threadID) + } + override fun getAuthToken(room: String, server: String): String? { val id = "$server.$room" return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id) } + override fun notifyConfigUpdates(forConfigObject: ConfigBase) { + notifyUpdates(forConfigObject) + } + + override fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean { + return configFactory.conversationInConfig(publicKey, groupPublicKey, openGroupId, visibleOnly) + } + + override fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean { + return configFactory.canPerformChange(variant, publicKey, changeTimestampMs) + } + + fun notifyUpdates(forConfigObject: ConfigBase) { + when (forConfigObject) { + is UserProfile -> updateUser(forConfigObject) + is Contacts -> updateContacts(forConfigObject) + is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject) + is UserGroupsConfig -> updateUserGroups(forConfigObject) + } + } + + private fun updateUser(userProfile: UserProfile) { + val userPublicKey = getUserPublicKey() ?: return + // would love to get rid of recipient and context from this + val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) + // update name + val name = userProfile.getName() ?: return + val userPic = userProfile.getPic() + val profileManager = SSKEnvironment.shared.profileManager + if (name.isNotEmpty()) { + TextSecurePreferences.setProfileName(context, name) + profileManager.setName(context, recipient, name) + } + + // update pfp + if (userPic == UserPic.DEFAULT) { + clearUserPic() + } else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty() + && TextSecurePreferences.getProfilePictureURL(context) != userPic.url) { + setUserProfilePicture(userPic.url, userPic.key) + } + if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) { + // delete nts thread if needed + val ourThread = getThreadId(recipient) ?: return + deleteConversation(ourThread) + } else { + // create note to self thread if needed (?) + val ourThread = getOrCreateThreadIdFor(recipient.address) + DatabaseComponent.get(context).threadDatabase().setHasSent(ourThread, true) + setPinned(ourThread, userProfile.getNtsPriority() > 0) + } + + } + + private fun updateContacts(contacts: Contacts) { + val extracted = contacts.all().toList() + addLibSessionContacts(extracted) + } + + override fun clearUserPic() { + val userPublicKey = getUserPublicKey() ?: return + val recipientDatabase = DatabaseComponent.get(context).recipientDatabase() + // would love to get rid of recipient and context from this + val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) + // clear picture if userPic is null + TextSecurePreferences.setProfileKey(context, null) + ProfileKeyUtil.setEncodedProfileKey(context, null) + recipientDatabase.setProfileAvatar(recipient, null) + TextSecurePreferences.setProfileAvatarId(context, 0) + TextSecurePreferences.setProfilePictureURL(context, null) + + Recipient.removeCached(fromSerialized(userPublicKey)) + configFactory.user?.setPic(UserPic.DEFAULT) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } + + private fun updateConvoVolatile(convos: ConversationVolatileConfig) { + val extracted = convos.all() + for (conversation in extracted) { + val threadId = when (conversation) { + is Conversation.OneToOne -> getThreadIdFor(conversation.sessionId, null, null, createThread = false) + is Conversation.LegacyGroup -> getThreadIdFor("", conversation.groupId,null, createThread = false) + is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", createThread = false) + } + if (threadId != null) { + if (conversation.lastRead > getLastSeen(threadId)) { + markConversationAsRead(threadId, conversation.lastRead, force = true) + } + updateThread(threadId, false) + } + } + } + + private fun updateUserGroups(userGroups: UserGroupsConfig) { + val threadDb = DatabaseComponent.get(context).threadDatabase() + val localUserPublicKey = getUserPublicKey() ?: return Log.w( + "Loki", + "No user public key when trying to update user groups from config" + ) + val communities = userGroups.allCommunityInfo() + val lgc = userGroups.allLegacyGroupInfo() + val allOpenGroups = getAllOpenGroups() + val toDeleteCommunities = allOpenGroups.filter { + Conversation.Community(BaseCommunityInfo(it.value.server, it.value.room, it.value.publicKey), 0, false).baseCommunityInfo.fullUrl() !in communities.map { it.community.fullUrl() } + } + + val existingCommunities: Map = allOpenGroups.filterKeys { it !in toDeleteCommunities.keys } + val toAddCommunities = communities.filter { it.community.fullUrl() !in existingCommunities.map { it.value.joinURL } } + val existingJoinUrls = existingCommunities.values.map { it.joinURL } + + val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isClosedGroup } + val lgcIds = lgc.map { it.sessionId } + val toDeleteClosedGroups = existingClosedGroups.filter { group -> + GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds + } + + // delete the ones which are not listed in the config + toDeleteCommunities.values.forEach { openGroup -> + OpenGroupManager.delete(openGroup.server, openGroup.room, context) + } + + toDeleteClosedGroups.forEach { deleteGroup -> + val threadId = getThreadId(deleteGroup.encodedId) + if (threadId != null) { + ClosedGroupManager.silentlyRemoveGroup(context,threadId,GroupUtil.doubleDecodeGroupId(deleteGroup.encodedId), deleteGroup.encodedId, localUserPublicKey, delete = true) + } + } + + toAddCommunities.forEach { toAddCommunity -> + val joinUrl = toAddCommunity.community.fullUrl() + if (!hasBackgroundGroupAddJob(joinUrl)) { + JobQueue.shared.add(BackgroundGroupAddJob(joinUrl)) + } + } + + for (groupInfo in communities) { + val groupBaseCommunity = groupInfo.community + if (groupBaseCommunity.fullUrl() in existingJoinUrls) { + // add it + val (threadId, _) = existingCommunities.entries.first { (_, v) -> v.joinURL == groupInfo.community.fullUrl() } + threadDb.setPinned(threadId, groupInfo.priority == PRIORITY_PINNED) + } + } + + for (group in lgc) { + val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId } + val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) } + if (existingGroup != null) { + if (group.priority == PRIORITY_HIDDEN && existingThread != null) { + ClosedGroupManager.silentlyRemoveGroup(context,existingThread,GroupUtil.doubleDecodeGroupId(existingGroup.encodedId), existingGroup.encodedId, localUserPublicKey, delete = true) + } else if (existingThread == null) { + Log.w("Loki-DBG", "Existing group had no thread to hide") + } else { + Log.d("Loki-DBG", "Setting existing group pinned status to ${group.priority}") + threadDb.setPinned(existingThread, group.priority == PRIORITY_PINNED) + } + } else { + val members = group.members.keys.map { Address.fromSerialized(it) } + val admins = group.members.filter { it.value /*admin = true*/ }.keys.map { Address.fromSerialized(it) } + val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId) + val title = group.name + val formationTimestamp = (group.joinedAt * 1000L) + createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp) + setProfileSharing(Address.fromSerialized(groupId), true) + // Add the group to the user's set of public keys to poll for + addClosedGroupPublicKey(group.sessionId) + // Store the encryption key pair + val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey)) + addClosedGroupEncryptionKeyPair(keyPair, group.sessionId, SnodeAPI.nowWithOffset) + // Set expiration timer + val expireTimer = group.disappearingTimer + setExpirationTimer(groupId, expireTimer.toInt()) + // Notify the PN server + PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey) + // Notify the user + val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId)) + threadDb.setDate(threadID, formationTimestamp) + insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp) + // Don't create config group here, it's from a config update + // Start polling + ClosedGroupPollerV2.shared.startPolling(group.sessionId) + } + } + } + override fun setAuthToken(room: String, server: String, newValue: String) { val id = "$server.$room" DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, newValue) @@ -324,6 +683,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue) } + override fun removeProfilePicture(groupID: String) { + DatabaseComponent.get(context).groupDatabase().removeProfilePicture(groupID) + } + override fun hasDownloadedProfilePicture(groupID: String): Boolean { return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID) } @@ -373,6 +736,22 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } + override fun markAsSyncing(timestamp: Long, author: String) { + DatabaseComponent.get(context).mmsSmsDatabase() + .getMessageFor(timestamp, author) + ?.run { getMmsDatabaseElseSms(isMms).markAsSyncing(id) } + } + + private fun getMmsDatabaseElseSms(isMms: Boolean) = + if (isMms) DatabaseComponent.get(context).mmsDatabase() + else DatabaseComponent.get(context).smsDatabase() + + override fun markAsResyncing(timestamp: Long, author: String) { + DatabaseComponent.get(context).mmsSmsDatabase() + .getMessageFor(timestamp, author) + ?.run { getMmsDatabaseElseSms(isMms).markAsResyncing(id) } + } + override fun markAsSending(timestamp: Long, author: String) { val database = DatabaseComponent.get(context).mmsSmsDatabase() val messageRecord = database.getMessageFor(timestamp, author) ?: return @@ -398,7 +777,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } - override fun setErrorMessage(timestamp: Long, author: String, error: Exception) { + override fun markAsSentFailed(timestamp: Long, author: String, error: Exception) { val database = DatabaseComponent.get(context).mmsSmsDatabase() val messageRecord = database.getMessageFor(timestamp, author) ?: return if (messageRecord.isMms) { @@ -421,6 +800,26 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } + override fun markAsSyncFailed(timestamp: Long, author: String, error: Exception) { + val database = DatabaseComponent.get(context).mmsSmsDatabase() + val messageRecord = database.getMessageFor(timestamp, author) ?: return + + database.getMessageFor(timestamp, author) + ?.run { getMmsDatabaseElseSms(isMms).markAsSyncFailed(id) } + + if (error.localizedMessage != null) { + val message: String + if (error is OnionRequestAPI.HTTPRequestFailedAtDestinationException && error.statusCode == 429) { + message = "429: Rate limited." + } else { + message = error.localizedMessage!! + } + DatabaseComponent.get(context).lokiMessageDatabase().setErrorMessage(messageRecord.getId(), message) + } else { + DatabaseComponent.get(context).lokiMessageDatabase().setErrorMessage(messageRecord.getId(), error.javaClass.simpleName) + } + } + override fun clearErrorMessage(messageID: Long) { val db = DatabaseComponent.get(context).lokiMessageDatabase() db.clearErrorMessage(messageID) @@ -439,6 +838,59 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseComponent.get(context).groupDatabase().create(groupId, title, members, avatar, relay, admins, formationTimestamp) } + override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) { + val volatiles = configFactory.convoVolatile ?: return + val userGroups = configFactory.userGroups ?: return + val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey) + groupVolatileConfig.lastRead = formationTimestamp + volatiles.set(groupVolatileConfig) + val groupInfo = GroupInfo.LegacyGroupInfo( + sessionId = groupPublicKey, + name = name, + members = members, + priority = ConfigBase.PRIORITY_VISIBLE, + encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte + encSecKey = encryptionKeyPair.privateKey.serialize(), + disappearingTimer = 0L, + joinedAt = (formationTimestamp / 1000L) + ) + // shouldn't exist, don't use getOrConstruct + copy + userGroups.set(groupInfo) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } + + override fun updateGroupConfig(groupPublicKey: String) { + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val groupAddress = fromSerialized(groupID) + // TODO: probably add a check in here for isActive? + // TODO: also check if local user is a member / maybe run delete otherwise? + val existingGroup = getGroup(groupID) + ?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config") + val userGroups = configFactory.userGroups ?: return + if (!existingGroup.isActive) { + userGroups.eraseLegacyGroup(groupPublicKey) + return + } + val name = existingGroup.title + val admins = existingGroup.admins.map { it.serialize() } + val members = existingGroup.members.map { it.serialize() } + val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members) + val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + ?: return Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config") + val recipientSettings = getRecipientSettings(groupAddress) ?: return + val threadID = getThreadId(groupAddress) ?: return + val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy( + name = name, + members = membersMap, + encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte + encSecKey = latestKeyPair.privateKey.serialize(), + priority = if (isPinned(threadID)) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, + disappearingTimer = recipientSettings.expireMessages.toLong(), + joinedAt = (existingGroup.formationTimestamp / 1000L) + ) + userGroups.set(groupInfo) + } + override fun isGroupActive(groupPublicKey: String): Boolean { return DatabaseComponent.get(context).groupDatabase().getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true } @@ -469,7 +921,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() val infoMessage = IncomingGroupMessage(m, groupID, updateData, true) val smsDB = DatabaseComponent.get(context).smsDatabase() - smsDB.insertMessageInbox(infoMessage, true, true) + smsDB.insertMessageInbox(infoMessage, true) } override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) { @@ -517,8 +969,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseComponent.get(context).lokiAPIDatabase().removeClosedGroupPublicKey(groupPublicKey) } - override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) { - DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) + override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) { + DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, timestamp) } override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) { @@ -535,9 +987,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, .updateTimestampUpdated(groupID, updatedTimestamp) } - override fun setExpirationTimer(groupID: String, duration: Int) { - val recipient = Recipient.from(context, fromSerialized(groupID), false) - DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration); + override fun setExpirationTimer(address: String, duration: Int) { + val recipient = Recipient.from(context, fromSerialized(address), false) + DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration) + if (recipient.isContactRecipient && !recipient.isLocalNumber) { + configFactory.contacts?.upsertContact(address) { + this.expiryMode = if (duration != 0) { + ExpiryMode.AfterRead(duration.toLong()) + } else { // = 0 / delete + ExpiryMode.NONE + } + } + if (configFactory.contacts?.needsPush() == true) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } + } } override fun setServerCapabilities(server: String, capabilities: List) { @@ -556,16 +1020,29 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, OpenGroupManager.updateOpenGroup(openGroup, context) } - override fun getAllGroups(): List { - return DatabaseComponent.get(context).groupDatabase().allGroups + override fun getAllGroups(includeInactive: Boolean): List { + return DatabaseComponent.get(context).groupDatabase().getAllGroups(includeInactive) } override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? { return OpenGroupManager.addOpenGroup(urlAsString, context) } - override fun onOpenGroupAdded(server: String) { + override fun onOpenGroupAdded(server: String, room: String) { OpenGroupManager.restartPollerForServer(server.removeSuffix("/")) + val groups = configFactory.userGroups ?: return + val volatileConfig = configFactory.convoVolatile ?: return + val openGroup = getOpenGroup(room, server) ?: return + val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return + val pubKeyHex = Hex.toStringCondensed(pubKey) + val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex) + groups.set(communityInfo) + val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey) + if (volatile.lastRead != 0L) { + val threadId = getThreadId(openGroup) ?: return + markConversationAsRead(threadId, volatile.lastRead, force = true) + } + volatileConfig.set(volatile) } override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean { @@ -583,17 +1060,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) } - override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long { + override fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? { val database = DatabaseComponent.get(context).threadDatabase() return if (!openGroupID.isNullOrEmpty()) { val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false) - database.getThreadIdIfExistsFor(recipient) + database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else if (!groupPublicKey.isNullOrEmpty()) { val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false) - database.getOrCreateThreadIdFor(recipient) + if (createThread) database.getOrCreateThreadIdFor(recipient) + else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else { val recipient = Recipient.from(context, fromSerialized(publicKey), false) - database.getOrCreateThreadIdFor(recipient) + if (createThread) database.getOrCreateThreadIdFor(recipient) + else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } } @@ -602,6 +1081,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return getThreadId(address) } + override fun getThreadId(openGroup: OpenGroup): Long? { + return GroupManager.getOpenGroupThreadID("${openGroup.server.removeSuffix("/")}.${openGroup.room}", context) + } + override fun getThreadId(address: Address): Long? { val recipient = Recipient.from(context, address, false) return getThreadId(recipient) @@ -631,6 +1114,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun setContact(contact: Contact) { DatabaseComponent.get(context).sessionContactDatabase().setContact(contact) + val address = fromSerialized(contact.sessionID) + if (!getRecipientApproved(address)) return + val recipientHash = SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact) + val recipient = Recipient.from(context, address, false) + setRecipientHash(recipient, recipientHash) } override fun getRecipientForThread(threadId: Long): Recipient? { @@ -646,6 +1134,51 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseComponent.get(context).recipientDatabase().isAutoDownloadFlagSet(recipient) } + override fun addLibSessionContacts(contacts: List) { + val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() + val moreContacts = contacts.filter { contact -> + val id = SessionId(contact.id) + id.prefix?.isBlinded() == false || mappingDb.getBlindedIdMapping(contact.id).none { it.sessionId != null } + } + val profileManager = SSKEnvironment.shared.profileManager + moreContacts.forEach { contact -> + val address = fromSerialized(contact.id) + val recipient = Recipient.from(context, address, false) + setBlocked(listOf(recipient), contact.blocked, fromConfigUpdate = true) + setRecipientApproved(recipient, contact.approved) + setRecipientApprovedMe(recipient, contact.approvedMe) + if (contact.name.isNotEmpty()) { + profileManager.setName(context, recipient, contact.name) + } else { + profileManager.setName(context, recipient, null) + } + if (contact.nickname.isNotEmpty()) { + profileManager.setNickname(context, recipient, contact.nickname) + } else { + profileManager.setNickname(context, recipient, null) + } + + if (contact.profilePicture != UserPic.DEFAULT) { + val (url, key) = contact.profilePicture + if (key.size != ProfileKeyUtil.PROFILE_KEY_BYTES) return@forEach + profileManager.setProfilePicture(context, recipient, url, key) + profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN) + } else { + profileManager.setProfilePicture(context, recipient, null, null) + } + if (contact.priority == PRIORITY_HIDDEN) { + getThreadId(fromSerialized(contact.id))?.let { conversationThreadId -> + deleteConversation(conversationThreadId) + } + } else { + getThreadId(fromSerialized(contact.id))?.let { conversationThreadId -> + setPinned(conversationThreadId, contact.priority == PRIORITY_PINNED) + } + } + setRecipientHash(recipient, contact.hashCode().toString()) + } + } + override fun addContacts(contacts: List) { val recipientDatabase = DatabaseComponent.get(context).recipientDatabase() val threadDatabase = DatabaseComponent.get(context).threadDatabase() @@ -669,17 +1202,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, recipientDatabase.setProfileSharing(recipient, true) recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED) // create Thread if needed - val threadId = threadDatabase.getOrCreateThreadIdFor(recipient) + val threadId = threadDatabase.getThreadIdIfExistsFor(recipient) if (contact.didApproveMe == true) { recipientDatabase.setApprovedMe(recipient, true) } - if (contact.isApproved == true) { - recipientDatabase.setApproved(recipient, true) + if (contact.isApproved == true && threadId != -1L) { + setRecipientApproved(recipient, true) threadDatabase.setHasSent(threadId, true) } - if (contact.isBlocked == true) { - recipientDatabase.setBlocked(recipient, true) - threadDatabase.deleteConversation(threadId) + + val contactIsBlocked: Boolean? = contact.isBlocked + if (contactIsBlocked != null && recipient.isBlocked != contactIsBlocked) { + setBlocked(listOf(recipient), contactIsBlocked, fromConfigUpdate = true) } } if (contacts.isNotEmpty()) { @@ -699,6 +1233,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, recipientDb.setAutoDownloadAttachments(recipient, shouldAutoDownloadAttachments) } + override fun setRecipientHash(recipient: Recipient, recipientHash: String?) { + val recipientDb = DatabaseComponent.get(context).recipientDatabase() + recipientDb.setRecipientHash(recipient, recipientHash) + } + override fun getLastUpdated(threadID: Long): Long { val threadDB = DatabaseComponent.get(context).threadDatabase() return threadDB.getLastUpdated(threadID) @@ -719,14 +1258,77 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return mmsSmsDb.getConversationCount(threadID) } - override fun setThreadPinned(threadID: Long, isPinned: Boolean) { - val threadDb = DatabaseComponent.get(context).threadDatabase() - threadDb.setPinned(threadID, isPinned) + override fun setPinned(threadID: Long, isPinned: Boolean) { + val threadDB = DatabaseComponent.get(context).threadDatabase() + threadDB.setPinned(threadID, isPinned) + val threadRecipient = getRecipientForThread(threadID) ?: return + if (threadRecipient.isLocalNumber) { + val user = configFactory.user ?: return + user.setNtsPriority(if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE) + } else if (threadRecipient.isContactRecipient) { + val contacts = configFactory.contacts ?: return + contacts.upsertContact(threadRecipient.address.serialize()) { + priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE + } + } else if (threadRecipient.isGroupRecipient) { + val groups = configFactory.userGroups ?: return + if (threadRecipient.isClosedGroupRecipient) { + val sessionId = GroupUtil.doubleDecodeGroupId(threadRecipient.address.serialize()) + val newGroupInfo = groups.getOrConstructLegacyGroupInfo(sessionId).copy ( + priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE + ) + groups.set(newGroupInfo) + } else if (threadRecipient.isOpenGroupRecipient) { + val openGroup = getOpenGroup(threadID) ?: return + val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return + val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy ( + priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE + ) + groups.set(newGroupInfo) + } + } + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } - override fun isThreadPinned(threadID: Long): Boolean { + override fun isPinned(threadID: Long): Boolean { + val threadDB = DatabaseComponent.get(context).threadDatabase() + return threadDB.isPinned(threadID) + } + + override fun setThreadDate(threadId: Long, newDate: Long) { val threadDb = DatabaseComponent.get(context).threadDatabase() - return threadDb.getPinned(threadID) + threadDb.setDate(threadId, newDate) + } + + override fun deleteConversation(threadID: Long) { + val recipient = getRecipientForThread(threadID) + val threadDB = DatabaseComponent.get(context).threadDatabase() + val groupDB = DatabaseComponent.get(context).groupDatabase() + threadDB.deleteConversation(threadID) + if (recipient != null) { + if (recipient.isContactRecipient) { + if (recipient.isLocalNumber) return + val contacts = configFactory.contacts ?: return + contacts.upsertContact(recipient.address.serialize()) { + this.priority = PRIORITY_HIDDEN + } + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } else if (recipient.isClosedGroupRecipient) { + // TODO: handle closed group + val volatile = configFactory.convoVolatile ?: return + val groups = configFactory.userGroups ?: return + val groupID = recipient.address.toGroupString() + val closedGroup = getGroup(groupID) + val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) + if (closedGroup != null) { + groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it) + volatile.eraseLegacyClosedGroup(groupPublicKey) + groups.eraseLegacyGroup(groupPublicKey) + } else { + Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}") + } + } + } } override fun clearMessages(threadID: Long, fromUser: Address?): Boolean { @@ -765,6 +1367,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, if (recipient.isBlocked) return + val threadId = getThreadId(recipient) ?: return + val mediaMessage = IncomingMediaMessage( address, sentTimestamp, @@ -783,14 +1387,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, Optional.of(message) ) - database.insertSecureDecryptedMessageInbox(mediaMessage, -1, runIncrement = true, runThreadUpdate = true) + database.insertSecureDecryptedMessageInbox(mediaMessage, threadId, runThreadUpdate = true) } override fun insertMessageRequestResponse(response: MessageRequestResponse) { val userPublicKey = getUserPublicKey() val senderPublicKey = response.sender!! val recipientPublicKey = response.recipient!! - if (userPublicKey == null || (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey)) return + + if ( + userPublicKey == null + || (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey) + // this is true if it is a sync message + || (userPublicKey == recipientPublicKey && userPublicKey == senderPublicKey) + ) return + val recipientDb = DatabaseComponent.get(context).recipientDatabase() val threadDB = DatabaseComponent.get(context).threadDatabase() if (userPublicKey == senderPublicKey) { @@ -802,7 +1413,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val mmsDb = DatabaseComponent.get(context).mmsDatabase() val smsDb = DatabaseComponent.get(context).smsDatabase() val sender = Recipient.from(context, fromSerialized(senderPublicKey), false) - val threadId = threadDB.getOrCreateThreadIdFor(sender) + val threadId = getOrCreateThreadIdFor(sender.address) val profile = response.profile if (profile != null) { val profileManager = SSKEnvironment.shared.profileManager @@ -817,9 +1428,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val profileKeyChanged = (sender.profileKey == null || !MessageDigest.isEqual(sender.profileKey, newProfileKey)) if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) { - profileManager.setProfileKey(context, sender, newProfileKey!!) + profileManager.setProfilePicture(context, sender, profile.profilePictureURL!!, newProfileKey!!) profileManager.setUnidentifiedAccessMode(context, sender, Recipient.UnidentifiedAccessMode.UNKNOWN) - profileManager.setProfilePictureURL(context, sender, profile.profilePictureURL!!) } } threadDB.setHasSent(threadId, true) @@ -876,16 +1486,28 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, Optional.absent(), Optional.absent() ) - mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runIncrement = true, runThreadUpdate = true) + mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runThreadUpdate = true) } } + override fun getRecipientApproved(address: Address): Boolean { + return DatabaseComponent.get(context).recipientDatabase().getApproved(address) + } + override fun setRecipientApproved(recipient: Recipient, approved: Boolean) { DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved) + if (recipient.isLocalNumber || !recipient.isContactRecipient) return + configFactory.contacts?.upsertContact(recipient.address.serialize()) { + this.approved = approved + } } override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) { DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe) + if (recipient.isLocalNumber || !recipient.isContactRecipient) return + configFactory.contacts?.upsertContact(recipient.address.serialize()) { + this.approvedMe = approvedMe + } } override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) { @@ -1015,14 +1637,22 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms)) } - override fun unblock(toUnblock: List) { + override fun setBlocked(recipients: Iterable, isBlocked: Boolean, fromConfigUpdate: Boolean) { val recipientDb = DatabaseComponent.get(context).recipientDatabase() - recipientDb.setBlocked(toUnblock, false) + recipientDb.setBlocked(recipients, isBlocked) + recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient -> + configFactory.contacts?.upsertContact(recipient.address.serialize()) { + this.blocked = isBlocked + } + } + val contactsConfig = configFactory.contacts ?: return + if (contactsConfig.needsPush() && !fromConfigUpdate) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } } override fun blockedContacts(): List { val recipientDb = DatabaseComponent.get(context).recipientDatabase() return recipientDb.blockedContacts } - } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 959cd82da..bd425ed93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -35,6 +35,7 @@ import com.annimon.stream.Stream; import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.jetbrains.annotations.NotNull; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Contact; import org.session.libsession.utilities.DelimiterUtil; @@ -49,7 +50,7 @@ import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Pair; import org.session.libsignal.utilities.guava.Optional; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.contactshare.ContactUtil; +import org.thoughtcrime.securesms.contacts.ContactUtil; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; @@ -57,14 +58,12 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; -import org.thoughtcrime.securesms.groups.OpenGroupMigrator; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.util.SessionMetaProtocol; import java.io.Closeable; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -74,6 +73,11 @@ import java.util.Set; public class ThreadDatabase extends Database { + public interface ConversationThreadUpdateListener { + void threadCreated(@NonNull Address address, long threadId); + void threadDeleted(@NonNull Address address, long threadId); + } + private static final String TAG = ThreadDatabase.class.getSimpleName(); private final Map addressCache = new HashMap<>(); @@ -141,13 +145,19 @@ public class ThreadDatabase extends Database { "ADD COLUMN " + UNREAD_MENTION_COUNT + " INTEGER DEFAULT 0;"; } + private ConversationThreadUpdateListener updateListener; + public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); } + public void setUpdateListener(ConversationThreadUpdateListener updateListener) { + this.updateListener = updateListener; + } + private long createThreadForRecipient(Address address, boolean group, int distributionType) { ContentValues contentValues = new ContentValues(4); - long date = System.currentTimeMillis(); + long date = SnodeAPI.getNowWithOffset(); contentValues.put(DATE, date - date % 1000); contentValues.put(ADDRESS, address.serialize()); @@ -207,10 +217,14 @@ public class ThreadDatabase extends Database { } private void deleteThread(long threadId) { + Recipient recipient = getRecipientForThreadId(threadId); SQLiteDatabase db = databaseHelper.getWritableDatabase(); - db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""}); + int numberRemoved = db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""}); addressCache.remove(threadId); notifyConversationListListeners(); + if (updateListener != null && numberRemoved > 0 && recipient != null) { + updateListener.threadDeleted(recipient.getAddress(), threadId); + } } private void deleteThreads(Set threadIds) { @@ -278,7 +292,7 @@ public class ThreadDatabase extends Database { DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); - update(threadId, false); + update(threadId, false, true); notifyConversationListeners(threadId); } } finally { @@ -291,10 +305,34 @@ public class ThreadDatabase extends Database { Log.i("ThreadDatabase", "Trimming thread: " + threadId + " before :"+timestamp); DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp); DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp); - update(threadId, false); + update(threadId, false, true); notifyConversationListeners(threadId); } + public List setRead(long threadId, long lastReadTime) { + + final List smsRecords = DatabaseComponent.get(context).smsDatabase().setMessagesRead(threadId, lastReadTime); + final List mmsRecords = DatabaseComponent.get(context).mmsDatabase().setMessagesRead(threadId, lastReadTime); + + if (smsRecords.isEmpty() && mmsRecords.isEmpty()) { + return Collections.emptyList(); + } + + ContentValues contentValues = new ContentValues(2); + contentValues.put(READ, smsRecords.isEmpty() && mmsRecords.isEmpty()); + contentValues.put(LAST_SEEN, lastReadTime); + + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""}); + + notifyConversationListListeners(); + + return new LinkedList() {{ + addAll(smsRecords); + addAll(mmsRecords); + }}; + } + public List setRead(long threadId, boolean lastSeen) { ContentValues contentValues = new ContentValues(1); contentValues.put(READ, 1); @@ -302,7 +340,7 @@ public class ThreadDatabase extends Database { contentValues.put(UNREAD_MENTION_COUNT, 0); if (lastSeen) { - contentValues.put(LAST_SEEN, System.currentTimeMillis()); + contentValues.put(LAST_SEEN, SnodeAPI.getNowWithOffset()); } SQLiteDatabase db = databaseHelper.getWritableDatabase(); @@ -319,30 +357,6 @@ public class ThreadDatabase extends Database { }}; } - public void incrementUnread(long threadId, int amount, int unreadMentionAmount) { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " + - UNREAD_COUNT + " = " + UNREAD_COUNT + " + ?, " + - UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " + ? WHERE " + ID + " = ?", - new String[] { - String.valueOf(amount), - String.valueOf(unreadMentionAmount), - String.valueOf(threadId) - }); - } - - public void decrementUnread(long threadId, int amount, int unreadMentionAmount) { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " + - UNREAD_COUNT + " = " + UNREAD_COUNT + " - ?, " + - UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " - ? WHERE " + ID + " = ? AND " + UNREAD_COUNT + " > 0", - new String[] { - String.valueOf(amount), - String.valueOf(unreadMentionAmount), - String.valueOf(threadId) - }); - } - public void setDistributionType(long threadId, int distributionType) { ContentValues contentValues = new ContentValues(1); contentValues.put(TYPE, distributionType); @@ -352,6 +366,14 @@ public class ThreadDatabase extends Database { notifyConversationListListeners(); } + public void setDate(long threadId, long date) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(DATE, date); + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""}); + if (updated > 0) notifyConversationListListeners(); + } + public int getDistributionType(long threadId) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); Cursor cursor = db.query(TABLE_NAME, new String[]{TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); @@ -427,9 +449,9 @@ public class ThreadDatabase extends Database { " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + - " WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + MESSAGE_COUNT + " = " + UNREAD_COUNT + " AND " + - RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " + + " WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " + + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL"; cursor = db.rawQuery(query, null); @@ -517,21 +539,50 @@ public class ThreadDatabase extends Database { return db.rawQuery(query, null); } - public void setLastSeen(long threadId, long timestamp) { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - ContentValues contentValues = new ContentValues(1); - if (timestamp == -1) { - contentValues.put(LAST_SEEN, System.currentTimeMillis()); - } else { - contentValues.put(LAST_SEEN, timestamp); - } + /** + * @param threadId + * @param timestamp + * @return true if we have set the last seen for the thread, false if there were no messages in the thread + */ + public boolean setLastSeen(long threadId, long timestamp) { + // edge case where we set the last seen time for a conversation before it loads messages (joining community for example) + MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); + Recipient forThreadId = getRecipientForThreadId(threadId); + if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isOpenGroupRecipient()) return false; + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + ContentValues contentValues = new ContentValues(1); + long lastSeenTime = timestamp == -1 ? SnodeAPI.getNowWithOffset() : timestamp; + contentValues.put(LAST_SEEN, lastSeenTime); + db.beginTransaction(); db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)}); + String smsCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.READ+" = 0"; + String smsMentionCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.READ+" = 0 AND s."+SmsDatabase.HAS_MENTION+" = 1"; + String smsReactionCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.REACTIONS_UNREAD+" = 1"; + String mmsCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.READ+" = 0"; + String mmsMentionCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.READ+" = 0 AND m."+MmsDatabase.HAS_MENTION+" = 1"; + String mmsReactionCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.REACTIONS_UNREAD+" = 1"; + String allSmsUnread = "(("+smsCountSubQuery+") + ("+smsReactionCountSubQuery+"))"; + String allMmsUnread = "(("+mmsCountSubQuery+") + ("+mmsReactionCountSubQuery+"))"; + String allUnread = "(("+allSmsUnread+") + ("+allMmsUnread+"))"; + String allUnreadMention = "(("+smsMentionCountSubQuery+") + ("+mmsMentionCountSubQuery+"))"; + + String reflectUpdates = "UPDATE "+TABLE_NAME+" AS t SET "+UNREAD_COUNT+" = "+allUnread+", "+UNREAD_MENTION_COUNT+" = "+allUnreadMention+" WHERE "+ID+" = ?"; + db.execSQL(reflectUpdates, new Object[]{threadId}); + db.setTransactionSuccessful(); + db.endTransaction(); + notifyConversationListeners(threadId); notifyConversationListListeners(); + return true; } - public void setLastSeen(long threadId) { - setLastSeen(threadId, -1); + /** + * @param threadId + * @return true if we have set the last seen for the thread, false if there were no messages in the thread + */ + public boolean setLastSeen(long threadId) { + return setLastSeen(threadId, -1); } public Pair getLastSeenAndHasSent(long threadId) { @@ -634,13 +685,19 @@ public class ThreadDatabase extends Database { try { cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null); - + long threadId; + boolean created = false; if (cursor != null && cursor.moveToFirst()) { - return cursor.getLong(cursor.getColumnIndexOrThrow(ID)); + threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); } else { DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, true); - return createThreadForRecipient(recipient.getAddress(), recipient.isGroupRecipient(), distributionType); + threadId = createThreadForRecipient(recipient.getAddress(), recipient.isGroupRecipient(), distributionType); + created = true; } + if (created && updateListener != null) { + updateListener.threadCreated(recipient.getAddress(), threadId); + } + return threadId; } finally { if (cursor != null) cursor.close(); @@ -679,13 +736,14 @@ public class ThreadDatabase extends Database { new String[] {String.valueOf(threadId)}); notifyConversationListeners(threadId); + notifyConversationListListeners(); } - public boolean update(long threadId, boolean unarchive) { + public boolean update(long threadId, boolean unarchive, boolean shouldDeleteOnEmpty) { MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); long count = mmsSmsDatabase.getConversationCount(threadId); - boolean shouldDeleteEmptyThread = deleteThreadOnEmpty(threadId); + boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && deleteThreadOnEmpty(threadId); if (count == 0 && shouldDeleteEmptyThread) { deleteThread(threadId); @@ -708,12 +766,10 @@ public class ThreadDatabase extends Database { updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record), record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(), record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount()); - notifyConversationListListeners(); return false; } else { if (shouldDeleteEmptyThread) { deleteThread(threadId); - notifyConversationListListeners(); return true; } else { updateThread(threadId, 0, "", null, System.currentTimeMillis(), 0, 0, 0, false, 0, 0); @@ -723,6 +779,8 @@ public class ThreadDatabase extends Database { } finally { if (reader != null) reader.close(); + notifyConversationListListeners(); + notifyConversationListeners(threadId); } } @@ -734,17 +792,32 @@ public class ThreadDatabase extends Database { new String[] {String.valueOf(threadId)}); notifyConversationListeners(threadId); + notifyConversationListListeners(); } - public boolean getPinned(long threadId) { - Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{IS_PINNED}, ID_WHERE, new String[] {""+threadId},null, null, null); - boolean isPinned = cursor.moveToNext() && cursor.getInt(0) == 1; - cursor.close(); - return isPinned; + public boolean isPinned(long threadId) { + SQLiteDatabase db = getReadableDatabase(); + Cursor cursor = db.query(TABLE_NAME, new String[]{IS_PINNED}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); + try { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0) == 1; + } + return false; + } finally { + if (cursor != null) cursor.close(); + } } - public void markAllAsRead(long threadId, boolean isGroupRecipient) { - List messages = setRead(threadId, true); + /** + * @param threadId + * @param isGroupRecipient + * @param lastSeenTime + * @return true if we have set the last seen for the thread, false if there were no messages in the thread + */ + public boolean markAllAsRead(long threadId, boolean isGroupRecipient, long lastSeenTime, boolean force) { + MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); + if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && !force) return false; + List messages = setRead(threadId, lastSeenTime); if (isGroupRecipient) { for (MarkedMessageInfo message: messages) { MarkReadReceiver.scheduleDeletion(context, message.getExpirationInfo()); @@ -752,7 +825,8 @@ public class ThreadDatabase extends Database { } else { MarkReadReceiver.process(context, messages); } - ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, false, 0); + ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, threadId); + return setLastSeen(threadId, lastSeenTime); } private boolean deleteThreadOnEmpty(long threadId) { @@ -810,77 +884,6 @@ public class ThreadDatabase extends Database { return query; } - @NotNull - public List getHttpOxenOpenGroups() { - String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; - String selection = OpenGroupMigrator.HTTP_PREFIX+OpenGroupMigrator.OPEN_GET_SESSION_TRAILING_DOT_ENCODED +"%"; - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = createQuery(where, 0); - Cursor cursor = db.rawQuery(query, new String[]{selection}); - - if (cursor == null) { - return Collections.emptyList(); - } - List threads = new ArrayList<>(); - try { - Reader reader = readerFor(cursor); - ThreadRecord record; - while ((record = reader.getNext()) != null) { - threads.add(record); - } - } finally { - cursor.close(); - } - return threads; - } - - @NotNull - public List getLegacyOxenOpenGroups() { - String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; - String selection = OpenGroupMigrator.LEGACY_GROUP_ENCODED_ID+"%"; - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = createQuery(where, 0); - Cursor cursor = db.rawQuery(query, new String[]{selection}); - - if (cursor == null) { - return Collections.emptyList(); - } - List threads = new ArrayList<>(); - try { - Reader reader = readerFor(cursor); - ThreadRecord record; - while ((record = reader.getNext()) != null) { - threads.add(record); - } - } finally { - cursor.close(); - } - return threads; - } - - @NotNull - public List getHttpsOxenOpenGroups() { - String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; - String selection = OpenGroupMigrator.NEW_GROUP_ENCODED_ID+"%"; - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = createQuery(where, 0); - Cursor cursor = db.rawQuery(query, new String[]{selection}); - if (cursor == null) { - return Collections.emptyList(); - } - List threads = new ArrayList<>(); - try { - Reader reader = readerFor(cursor); - ThreadRecord record; - while ((record = reader.getNext()) != null) { - threads.add(record); - } - } finally { - cursor.close(); - } - return threads; - } - public void migrateEncodedGroup(long threadId, @NotNull String newEncodedGroupId) { ContentValues contentValues = new ContentValues(1); contentValues.put(ADDRESS, newEncodedGroupId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index fe1b9f785..0c0ebb01d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -11,7 +11,6 @@ import androidx.core.app.NotificationCompat; import net.zetetic.database.sqlcipher.SQLiteConnection; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; -import net.zetetic.database.sqlcipher.SQLiteException; import net.zetetic.database.sqlcipher.SQLiteOpenHelper; import org.session.libsession.utilities.TextSecurePreferences; @@ -19,12 +18,12 @@ import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.BlindedIdMappingDatabase; +import org.thoughtcrime.securesms.database.ConfigDatabase; import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.EmojiSearchDatabase; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupMemberDatabase; 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.LokiMessageDatabase; @@ -40,6 +39,7 @@ import org.thoughtcrime.securesms.database.SessionJobDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.notifications.NotificationChannels; +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities; import java.io.File; @@ -87,9 +87,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV39 = 60; private static final int lokiV40 = 61; private static final int lokiV41 = 62; + private static final int lokiV42 = 63; + private static final int lokiV43 = 64; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV41; + private static final int DATABASE_VERSION = lokiV43; private static final int MIN_DATABASE_VERSION = lokiV7; private static final String CIPHER3_DATABASE_NAME = "signal.db"; public static final String DATABASE_NAME = "signal_v4.db"; @@ -98,25 +100,40 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private final DatabaseSecret databaseSecret; public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) { - super(context, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, MIN_DATABASE_VERSION, null, new SQLiteDatabaseHook() { - @Override - public void preKey(SQLiteConnection connection) { - SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); - } - - @Override - public void postKey(SQLiteConnection connection) { - SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); - - // if not vacuumed in a while, perform that operation - long currentTime = System.currentTimeMillis(); - // 7 days - if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) { - connection.execute("VACUUM;", null, null); - TextSecurePreferences.setLastVacuumNow(context); + super( + context, + DATABASE_NAME, + databaseSecret.asString(), + null, + DATABASE_VERSION, + MIN_DATABASE_VERSION, + null, + new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteConnection connection) { + SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); } - } - }, true); + + @Override + public void postKey(SQLiteConnection connection) { + SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); + + // if not vacuumed in a while, perform that operation + long currentTime = System.currentTimeMillis(); + // 7 days + if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) { + connection.execute("VACUUM;", null, null); + TextSecurePreferences.setLastVacuumNow(context); + } + } + }, + // Note: Now that we support concurrent database reads the migrations are actually non-blocking + // because of this we need to initially open the database with writeAheadLogging (WAL mode) disabled + // and enable it once the database officially opens it's connection (which will cause it to re-connect + // in WAL mode) - this is a little inefficient but will prevent SQL-related errors/crashes due to + // incomplete migrations + false + ); this.context = context.getApplicationContext(); this.databaseSecret = databaseSecret; @@ -134,7 +151,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { connection.execute("PRAGMA cipher_page_size = 4096;", null, null); } - private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) throws SQLiteException { + private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) { return SQLiteDatabase.openDatabase(path, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() { @Override public void preKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); } @@ -151,11 +168,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { // If the old SQLCipher3 database file doesn't exist then no need to do anything if (!oldDbFile.exists()) { return; } - try { - // Define the location for the new database - String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath(); - File newDbFile = new File(newDbPath); + // Define the location for the new database + String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath(); + File newDbFile = new File(newDbPath); + try { // If the new database file already exists then check if it's valid first, if it's in an // invalid state we should delete it and try to migrate again if (newDbFile.exists()) { @@ -163,10 +180,24 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { // assume the user hasn't downgraded for some reason and made changes to the old database and // can remove the old database file (it won't be used anymore) if (oldDbFile.lastModified() <= newDbFile.lastModified()) { - // TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past -// //noinspection ResultOfMethodCallIgnored -// oldDbFile.delete(); - return; + try { + SQLiteDatabase newDb = SQLCipherOpenHelper.open(newDbPath, databaseSecret, true); + int version = newDb.getVersion(); + newDb.close(); + + // Make sure the new database has it's version set correctly (if not then the migration didn't + // fully succeed and the database will try to create all it's tables and immediately fail so + // we will need to remove and remigrate) + if (version > 0) { + // TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past +// //noinspection ResultOfMethodCallIgnored +// oldDbFile.delete(); + return; + } + } + catch (Exception e) { + Log.i(TAG, "Failed to retrieve version from new database, assuming invalid and remigrating"); + } } // If the old database does have newer changes then the new database could have stale/invalid @@ -208,6 +239,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { catch (Exception e) { Log.e(TAG, "Migration from SQLCipher3 to SQLCipher4 failed", e); + // If an exception was thrown then we should remove the new database file (it's probably invalid) + if (!newDbFile.delete()) { + Log.e(TAG, "Unable to delete invalid new database file"); + } + // Notify the user of the issue so they know they can downgrade until the issue is fixed NotificationManager notificationManager = context.getSystemService(NotificationManager.class); String channelId = context.getString(R.string.NotificationChannel_failures); @@ -251,9 +287,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { for (String sql : SearchDatabase.CREATE_TABLE) { db.execSQL(sql); } - for (String sql : JobDatabase.CREATE_TABLE) { - db.execSQL(sql); - } db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand()); db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand()); db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand()); @@ -311,6 +344,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(ThreadDatabase.getUnreadMentionCountCommand()); db.execSQL(SmsDatabase.CREATE_HAS_MENTION_COMMAND); db.execSQL(MmsDatabase.CREATE_HAS_MENTION_COMMAND); + db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -322,6 +356,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { executeStatements(db, ReactionDatabase.CREATE_INDEXS); executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS); + db.execSQL(RecipientDatabase.getAddWrapperHash()); db.execSQL(RecipientDatabase.getCreateAutoDownloadCommand()); db.execSQL(RecipientDatabase.getUpdateAutoDownloadValuesCommand()); @@ -558,6 +593,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } if (oldVersion < lokiV41) { + db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND); + db.execSQL(ConfigurationMessageUtilities.DELETE_INACTIVE_GROUPS); + db.execSQL(ConfigurationMessageUtilities.DELETE_INACTIVE_ONE_TO_ONES); + } + + if (oldVersion < lokiV42) { + db.execSQL(RecipientDatabase.getAddWrapperHash()); + } + + if (oldVersion < lokiV43) { db.execSQL(RecipientDatabase.getCreateAutoDownloadCommand()); db.execSQL(RecipientDatabase.getUpdateAutoDownloadValuesCommand()); } @@ -568,6 +613,15 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } } + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + + // Now that the database is officially open (ie. the migrations are completed) we want to enable + // write ahead logging (WAL mode) to officially support concurrent read connections + db.enableWriteAheadLogging(); + } + public void markCurrent(SQLiteDatabase db) { db.setVersion(DATABASE_VERSION); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index ef0f4b54f..39fba182a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -80,6 +80,18 @@ public abstract class DisplayRecord { return !isFailed() && !isPending(); } + public boolean isSyncing() { + return MmsSmsColumns.Types.isSyncingType(type); + } + + public boolean isResyncing() { + return MmsSmsColumns.Types.isResyncingType(type); + } + + public boolean isSyncFailed() { + return MmsSmsColumns.Types.isSyncFailedMessageType(type); + } + public boolean isFailed() { return MmsSmsColumns.Types.isFailedMessageType(type) || MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index dfc4c1bc8..f3e72a874 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -51,6 +51,7 @@ public class ThreadRecord extends DisplayRecord { private final long expiresIn; private final long lastSeen; private final boolean pinned; + private final int initialRecipientHash; public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, @NonNull Recipient recipient, long date, long count, int unreadCount, @@ -68,6 +69,7 @@ public class ThreadRecord extends DisplayRecord { this.expiresIn = expiresIn; this.lastSeen = lastSeen; this.pinned = pinned; + this.initialRecipientHash = recipient.hashCode(); } public @Nullable Uri getSnippetUri() { @@ -176,4 +178,8 @@ public class ThreadRecord extends DisplayRecord { public boolean isPinned() { return pinned; } + + public int getInitialRecipientHash() { + return initialRecipientHash; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt index 6f26c6ae3..936e4f287 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.dependencies import dagger.Binds import dagger.Module +import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.session.libsession.utilities.AppTextSecurePreferences @@ -19,4 +20,10 @@ abstract class AppModule { @Binds abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository +} + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface AppComponent { + fun getPrefs(): TextSecurePreferences } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt new file mode 100644 index 000000000..d664ffedb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -0,0 +1,251 @@ +package org.thoughtcrime.securesms.dependencies + +import android.content.Context +import android.os.Trace +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.UserGroupsConfig +import network.loki.messenger.libsession_util.UserProfile +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.ConfigFactoryUpdateListener +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.ConfigDatabase +import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get +import org.thoughtcrime.securesms.groups.GroupManager +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities + +class ConfigFactory( + private val context: Context, + private val configDatabase: ConfigDatabase, + private val maybeGetUserInfo: () -> Pair? +) : + ConfigFactoryProtocol { + companion object { + // This is a buffer period within which we will process messages which would result in a + // config change, any message which would normally result in a config change which was sent + // before `lastConfigMessage.timestamp - configChangeBufferPeriod` will not actually have + // it's changes applied (control text will still be added though) + val configChangeBufferPeriod: Long = (2 * 60 * 1000) + } + + fun keyPairChanged() { // this should only happen restoring or clearing data + _userConfig?.free() + _contacts?.free() + _convoVolatileConfig?.free() + _userConfig = null + _contacts = null + _convoVolatileConfig = null + } + + private val userLock = Object() + private var _userConfig: UserProfile? = null + private val contactsLock = Object() + private var _contacts: Contacts? = null + private val convoVolatileLock = Object() + private var _convoVolatileConfig: ConversationVolatileConfig? = null + private val userGroupsLock = Object() + private var _userGroups: UserGroupsConfig? = null + + private val isConfigForcedOn by lazy { TextSecurePreferences.hasForcedNewConfig(context) } + + private val listeners: MutableList = mutableListOf() + fun registerListener(listener: ConfigFactoryUpdateListener) { + listeners += listener + } + + fun unregisterListener(listener: ConfigFactoryUpdateListener) { + listeners -= listener + } + + private inline fun synchronizedWithLog(lock: Any, body: ()->T): T { + Trace.beginSection("synchronizedWithLog") + val result = synchronized(lock) { + body() + } + Trace.endSection() + return result + } + + override val user: UserProfile? + get() = synchronizedWithLog(userLock) { + if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null + if (_userConfig == null) { + val (secretKey, publicKey) = maybeGetUserInfo() ?: return null + val userDump = configDatabase.retrieveConfigAndHashes( + SharedConfigMessage.Kind.USER_PROFILE.name, + publicKey + ) + _userConfig = if (userDump != null) { + UserProfile.newInstance(secretKey, userDump) + } else { + ConfigurationMessageUtilities.generateUserProfileConfigDump()?.let { dump -> + UserProfile.newInstance(secretKey, dump) + } ?: UserProfile.newInstance(secretKey) + } + } + _userConfig + } + + override val contacts: Contacts? + get() = synchronizedWithLog(contactsLock) { + if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null + if (_contacts == null) { + val (secretKey, publicKey) = maybeGetUserInfo() ?: return null + val contactsDump = configDatabase.retrieveConfigAndHashes( + SharedConfigMessage.Kind.CONTACTS.name, + publicKey + ) + _contacts = if (contactsDump != null) { + Contacts.newInstance(secretKey, contactsDump) + } else { + ConfigurationMessageUtilities.generateContactConfigDump()?.let { dump -> + Contacts.newInstance(secretKey, dump) + } ?: Contacts.newInstance(secretKey) + } + } + _contacts + } + + override val convoVolatile: ConversationVolatileConfig? + get() = synchronizedWithLog(convoVolatileLock) { + if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null + if (_convoVolatileConfig == null) { + val (secretKey, publicKey) = maybeGetUserInfo() ?: return null + val convoDump = configDatabase.retrieveConfigAndHashes( + SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name, + publicKey + ) + _convoVolatileConfig = if (convoDump != null) { + ConversationVolatileConfig.newInstance(secretKey, convoDump) + } else { + ConfigurationMessageUtilities.generateConversationVolatileDump(context) + ?.let { dump -> + ConversationVolatileConfig.newInstance(secretKey, dump) + } ?: ConversationVolatileConfig.newInstance(secretKey) + } + } + _convoVolatileConfig + } + + override val userGroups: UserGroupsConfig? + get() = synchronizedWithLog(userGroupsLock) { + if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null + if (_userGroups == null) { + val (secretKey, publicKey) = maybeGetUserInfo() ?: return null + val userGroupsDump = configDatabase.retrieveConfigAndHashes( + SharedConfigMessage.Kind.GROUPS.name, + publicKey + ) + _userGroups = if (userGroupsDump != null) { + UserGroupsConfig.Companion.newInstance(secretKey, userGroupsDump) + } else { + ConfigurationMessageUtilities.generateUserGroupDump(context)?.let { dump -> + UserGroupsConfig.Companion.newInstance(secretKey, dump) + } ?: UserGroupsConfig.newInstance(secretKey) + } + } + _userGroups + } + + override fun getUserConfigs(): List = + listOfNotNull(user, contacts, convoVolatile, userGroups) + + + private fun persistUserConfigDump(timestamp: Long) = synchronized(userLock) { + val dumped = user?.dump() ?: return + val (_, publicKey) = maybeGetUserInfo() ?: return + configDatabase.storeConfig(SharedConfigMessage.Kind.USER_PROFILE.name, publicKey, dumped, timestamp) + } + + private fun persistContactsConfigDump(timestamp: Long) = synchronized(contactsLock) { + val dumped = contacts?.dump() ?: return + val (_, publicKey) = maybeGetUserInfo() ?: return + configDatabase.storeConfig(SharedConfigMessage.Kind.CONTACTS.name, publicKey, dumped, timestamp) + } + + private fun persistConvoVolatileConfigDump(timestamp: Long) = synchronized(convoVolatileLock) { + val dumped = convoVolatile?.dump() ?: return + val (_, publicKey) = maybeGetUserInfo() ?: return + configDatabase.storeConfig( + SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name, + publicKey, + dumped, + timestamp + ) + } + + private fun persistUserGroupsConfigDump(timestamp: Long) = synchronized(userGroupsLock) { + val dumped = userGroups?.dump() ?: return + val (_, publicKey) = maybeGetUserInfo() ?: return + configDatabase.storeConfig(SharedConfigMessage.Kind.GROUPS.name, publicKey, dumped, timestamp) + } + + override fun persist(forConfigObject: ConfigBase, timestamp: Long) { + try { + listeners.forEach { listener -> + listener.notifyUpdates(forConfigObject) + } + when (forConfigObject) { + is UserProfile -> persistUserConfigDump(timestamp) + is Contacts -> persistContactsConfigDump(timestamp) + is ConversationVolatileConfig -> persistConvoVolatileConfigDump(timestamp) + is UserGroupsConfig -> persistUserGroupsConfigDump(timestamp) + else -> throw UnsupportedOperationException("Can't support type of ${forConfigObject::class.simpleName} yet") + } + } catch (e: Exception) { + Log.e("Loki", "failed to persist ${forConfigObject.javaClass.simpleName}", e) + } + } + + override fun conversationInConfig( + publicKey: String?, + groupPublicKey: String?, + openGroupId: String?, + visibleOnly: Boolean + ): Boolean { + if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return true + + val (_, userPublicKey) = maybeGetUserInfo() ?: return true + + if (openGroupId != null) { + val userGroups = userGroups ?: return false + val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context) + val openGroup = get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false + + // Not handling the `hidden` behaviour for communities so just indicate the existence + return (userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null) + } + else if (groupPublicKey != null) { + val userGroups = userGroups ?: return false + + // Not handling the `hidden` behaviour for legacy groups so just indicate the existence + return (userGroups.getLegacyGroupInfo(groupPublicKey) != null) + } + else if (publicKey == userPublicKey) { + val user = user ?: return false + + return (!visibleOnly || user.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN) + } + else if (publicKey != null) { + val contacts = contacts ?: return false + val targetContact = contacts.get(publicKey) ?: return false + + return (!visibleOnly || targetContact.priority != ConfigBase.PRIORITY_HIDDEN) + } + + return false + } + + override fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean { + if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return true + + val lastUpdateTimestampMs = configDatabase.retrieveConfigLastUpdateTimestamp(variant, publicKey) + + // Ensure the change occurred after the last config message was handled (minus the buffer period) + return (changeTimestampMs >= (lastUpdateTimestampMs - ConfigFactory.configChangeBufferPeriod)) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt index 6ea3b0fb9..eddd61c83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt @@ -33,7 +33,6 @@ interface DatabaseComponent { fun recipientDatabase(): RecipientDatabase fun groupReceiptDatabase(): GroupReceiptDatabase fun searchDatabase(): SearchDatabase - fun jobDatabase(): JobDatabase fun lokiAPIDatabase(): LokiAPIDatabase fun lokiMessageDatabase(): LokiMessageDatabase fun lokiThreadDatabase(): LokiThreadDatabase @@ -47,4 +46,5 @@ interface DatabaseComponent { fun attachmentProvider(): MessageDataProvider fun blindedIdMappingDatabase(): BlindedIdMappingDatabase fun groupMemberDatabase(): GroupMemberDatabase + fun configDatabase(): ConfigDatabase } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt index 35ccb0f55..6580c5c92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt @@ -6,7 +6,6 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import net.zetetic.database.sqlcipher.SQLiteDatabase import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider @@ -87,10 +86,6 @@ object DatabaseModule { @Singleton fun searchDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = SearchDatabase(context,openHelper) - @Provides - @Singleton - fun provideJobDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = JobDatabase(context, openHelper) - @Provides @Singleton fun provideLokiApiDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = LokiAPIDatabase(context,openHelper) @@ -137,10 +132,18 @@ object DatabaseModule { @Provides @Singleton - fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): StorageProtocol = Storage(context,openHelper) + fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper, configFactory: ConfigFactory, threadDatabase: ThreadDatabase): Storage { + val storage = Storage(context,openHelper, configFactory) + threadDatabase.setUpdateListener(storage) + return storage + } @Provides @Singleton fun provideAttachmentProvider(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): MessageDataProvider = DatabaseAttachmentProvider(context, openHelper) + @Provides + @Singleton + fun provideConfigDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): ConfigDatabase = ConfigDatabase(context, openHelper) + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java deleted file mode 100644 index 033b3ef45..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.thoughtcrime.securesms.dependencies; - -public interface InjectableType { -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt new file mode 100644 index 000000000..cd4b07133 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt @@ -0,0 +1,36 @@ +package org.thoughtcrime.securesms.dependencies + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import org.session.libsession.utilities.ConfigFactoryUpdateListener +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.database.ConfigDatabase +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SessionUtilModule { + + private fun maybeUserEdSecretKey(context: Context): ByteArray? { + val edKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return null + return edKey.secretKey.asBytes + } + + @Provides + @Singleton + fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory = + ConfigFactory(context, configDatabase) { + val localUserPublicKey = TextSecurePreferences.getLocalNumber(context) + val secretKey = maybeUserEdSecretKey(context) + if (localUserPublicKey == null || secretKey == null) null + else secretKey to localUserPublicKey + }.apply { + registerListener(context as ConfigFactoryUpdateListener) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt index 8b880d218..74e2cac4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt @@ -98,7 +98,7 @@ class NewMessageFragment : Fragment() { private fun hideLoader() { binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) binding.loader.visibility = View.GONE } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt new file mode 100644 index 000000000..f8e64dd38 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt @@ -0,0 +1,64 @@ +package org.thoughtcrime.securesms.groups + +import android.content.Context +import network.loki.messenger.libsession_util.ConfigBase +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 +import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.GroupRecord +import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.crypto.ecc.DjbECPublicKey +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.dependencies.ConfigFactory + +object ClosedGroupManager { + + fun silentlyRemoveGroup(context: Context, threadId: Long, groupPublicKey: String, groupID: String, userPublicKey: String, delete: Boolean = true) { + val storage = MessagingModuleConfiguration.shared.storage + // Mark the group as inactive + storage.setActive(groupID, false) + storage.removeClosedGroupPublicKey(groupPublicKey) + // Remove the key pairs + storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) + storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) + // Notify the PN server + PushRegistryV1.unsubscribeGroup(closedGroupPublicKey = groupPublicKey, publicKey = userPublicKey) + // Stop polling + ClosedGroupPollerV2.shared.stopPolling(groupPublicKey) + storage.cancelPendingMessageSendJobs(threadId) + ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) + if (delete) { + storage.deleteConversation(threadId) + } + } + + fun ConfigFactory.removeLegacyGroup(group: GroupRecord): Boolean { + val groups = userGroups ?: return false + if (!group.isClosedGroup) return false + val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId()) + return groups.eraseLegacyGroup(groupPublicKey) + } + + fun ConfigFactory.updateLegacyGroup(groupRecipientSettings: Recipient.RecipientSettings, group: GroupRecord) { + val groups = userGroups ?: return + if (!group.isClosedGroup) return + val storage = MessagingModuleConfiguration.shared.storage + val threadId = storage.getThreadId(group.encodedId) ?: return + val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId()) + val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return + val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey) + val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize)) + val toSet = legacyInfo.copy( + members = latestMemberMap, + name = group.title, + disappearingTimer = groupRecipientSettings.expireMessages.toLong(), + priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, + encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte + encSecKey = latestKeyPair.privateKey.serialize() + ) + groups.set(toSet) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt index ead979b77..ecd40938a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt @@ -21,6 +21,7 @@ import nl.komponents.kovenant.ui.successUi import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.groupSizeLimit import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Device import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.contacts.SelectContactsAdapter @@ -31,10 +32,14 @@ import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeOut +import javax.inject.Inject @AndroidEntryPoint class CreateGroupFragment : Fragment() { + @Inject + lateinit var device: Device + private lateinit var binding: FragmentCreateGroupBinding private val viewModel: CreateGroupViewModel by viewModels() @@ -86,7 +91,7 @@ class CreateGroupFragment : Fragment() { val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!! isLoading = true binding.loaderContainer.fadeIn() - MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> + MessageSender.createClosedGroup(device, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> binding.loaderContainer.fadeOut() isLoading = false val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(requireContext(), Address.fromSerialized(groupID), false)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt index 62e762316..9fee8adaf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt @@ -16,6 +16,7 @@ import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import nl.komponents.kovenant.Promise import nl.komponents.kovenant.task @@ -28,16 +29,28 @@ import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.contacts.SelectContactsActivity +import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeOut import java.io.IOException +import javax.inject.Inject +@AndroidEntryPoint class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { + + @Inject + lateinit var groupConfigFactory: ConfigFactory + @Inject + lateinit var storage: Storage + private val originalMembers = HashSet() private val zombies = HashSet() private val members = HashSet() @@ -289,7 +302,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { isLoading = true loaderContainer.fadeIn() val promise: Promise = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) { - MessageSender.explicitLeave(groupPublicKey!!, true) + MessageSender.explicitLeave(groupPublicKey!!, false) } else { task { if (hasNameChanged) { @@ -306,6 +319,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { promise.successUi { loaderContainer.fadeOut() isLoading = false + updateGroupConfig() finish() }.failUi { exception -> val message = if (exception is MessageSender.Error) exception.description else "An error occurred" @@ -316,5 +330,13 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { } } - class GroupMembers(val members: List, val zombieMembers: List) { } + private fun updateGroupConfig() { + val latestRecipient = storage.getRecipientSettings(Address.fromSerialized(groupID)) + ?: return Log.w("Loki", "No recipient settings when trying to update group config") + val latestGroup = storage.getGroup(groupID) + ?: return Log.w("Loki", "No group record when trying to update group config") + groupConfigFactory.updateLegacyGroup(latestRecipient, latestGroup) + } + + class GroupMembers(val members: List, val zombieMembers: List) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index a3d0e6d25..d4c5acf4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -6,6 +6,7 @@ import android.graphics.Bitmap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.DistributionTypes; import org.session.libsession.utilities.GroupUtil; @@ -16,11 +17,14 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.util.BitmapUtil; +import java.io.IOException; import java.util.HashSet; import java.util.LinkedList; import java.util.Objects; import java.util.Set; +import network.loki.messenger.libsession_util.UserGroupsConfig; + public class GroupManager { public static long getOpenGroupThreadID(String id, @NonNull Context context) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt index d37b17ef9..ae59c3833 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt @@ -55,7 +55,7 @@ class JoinCommunityFragment : Fragment() { fun hideLoader() { binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) binding.loader.visibility = View.GONE } @@ -79,7 +79,7 @@ class JoinCommunityFragment : Fragment() { val openGroupID = "$sanitizedServer.${openGroup.room}" OpenGroupManager.add(sanitizedServer, openGroup.room, openGroup.serverPublicKey, requireContext()) val storage = MessagingModuleConfiguration.shared.storage - storage.onOpenGroupAdded(sanitizedServer) + storage.onOpenGroupAdded(sanitizedServer, openGroup.room) val threadID = GroupManager.getOpenGroupThreadID(openGroupID, requireContext()) val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index ef4726910..2754c70f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -9,13 +9,13 @@ import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import java.util.concurrent.Executors object OpenGroupManager { private val executorService = Executors.newScheduledThreadPool(4) - private var pollers = mutableMapOf() // One for each server + private val pollers = mutableMapOf() // One for each server private var isPolling = false private val pollUpdaterLock = Any() @@ -40,12 +40,18 @@ object OpenGroupManager { if (isPolling) { return } isPolling = true val storage = MessagingModuleConfiguration.shared.storage - val servers = storage.getAllOpenGroups().values.map { it.server }.toSet() - servers.forEach { server -> - pollers[server]?.stop() // Shouldn't be necessary - val poller = OpenGroupPoller(server, executorService) - poller.startIfNeeded() - pollers[server] = poller + val (serverGroups, toDelete) = storage.getAllOpenGroups().values.partition { storage.getThreadId(it) != null } + toDelete.forEach { openGroup -> + Log.w("Loki", "Need to delete a group") + delete(openGroup.server, openGroup.room, MessagingModuleConfiguration.shared.context) + } + + val servers = serverGroups.map { it.server }.toSet() + synchronized(pollUpdaterLock) { + servers.forEach { server -> + pollers[server]?.stop() // Shouldn't be necessary + pollers[server] = OpenGroupPoller(server, executorService).apply { startIfNeeded() } + } } } @@ -58,14 +64,14 @@ object OpenGroupManager { } @WorkerThread - fun add(server: String, room: String, publicKey: String, context: Context): OpenGroupApi.RoomInfo? { + fun add(server: String, room: String, publicKey: String, context: Context): Pair { val openGroupID = "$server.$room" - var threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) + val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) val storage = MessagingModuleConfiguration.shared.storage val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() // Check it it's added already val existingOpenGroup = threadDB.getOpenGroupChat(threadID) - if (existingOpenGroup != null) { return null } + if (existingOpenGroup != null) { return threadID to null } // Clear any existing data if needed storage.removeLastDeletionServerID(room, server) storage.removeLastMessageServerID(room, server) @@ -76,14 +82,17 @@ object OpenGroupManager { // Get capabilities & room info val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get() storage.setServerCapabilities(server, capabilities.capabilities) - storage.setUserCount(room, server, info.activeUsers) // Create the group locally if not available already if (threadID < 0) { - threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId + GroupManager.createOpenGroup(openGroupID, context, null, info.name) } - val openGroup = OpenGroup(server = server, room = room, publicKey = publicKey, name = info.name, imageId = info.imageId, canWrite = info.write, infoUpdates = info.infoUpdates) - threadDB.setOpenGroupChat(openGroup, threadID) - return info + OpenGroupPoller.handleRoomPollInfo( + server = server, + roomToken = room, + pollInfo = info.toPollInfo(), + createGroupIfMissingWithPublicKey = publicKey + ) + return threadID to info } fun restartPollerForServer(server: String) { @@ -99,23 +108,27 @@ object OpenGroupManager { } } + @WorkerThread fun delete(server: String, room: String, context: Context) { val storage = MessagingModuleConfiguration.shared.storage + val configFactory = MessagingModuleConfiguration.shared.configFactory val threadDB = DatabaseComponent.get(context).threadDatabase() - val openGroupID = "$server.$room" + val openGroupID = "${server.removeSuffix("/")}.$room" val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) val recipient = threadDB.getRecipientForThreadId(threadID) ?: return threadDB.setThreadArchived(threadID) val groupID = recipient.address.serialize() // Stop the poller if needed val openGroups = storage.getAllOpenGroups().filter { it.value.server == server } - if (openGroups.count() == 1) { + if (openGroups.isNotEmpty()) { synchronized(pollUpdaterLock) { val poller = pollers[server] poller?.stop() pollers.remove(server) } } + configFactory.userGroups?.eraseCommunity(server, room) + configFactory.convoVolatile?.eraseCommunity(server, room) // Delete storage.removeLastDeletionServerID(room, server) storage.removeLastMessageServerID(room, server) @@ -123,19 +136,19 @@ object OpenGroupManager { storage.removeLastOutboxMessageId(server) val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase() lokiThreadDB.removeOpenGroupChat(threadID) - ThreadUtils.queue { - threadDB.deleteConversation(threadID) // Must be invoked on a background thread - GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread - } + storage.deleteConversation(threadID) // Must be invoked on a background thread + GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } + @WorkerThread fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? { val url = HttpUrl.parse(urlAsString) ?: return null val server = OpenGroup.getServer(urlAsString) val room = url.pathSegments().firstOrNull() ?: return null val publicKey = url.queryParameter("public_key") ?: return null - return add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function + return add(server.toString().removeSuffix("/"), room, publicKey, context).second // assume migrated from calling function } fun updateOpenGroup(openGroup: OpenGroup, context: Context) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt deleted file mode 100644 index 642d19161..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt +++ /dev/null @@ -1,139 +0,0 @@ -package org.thoughtcrime.securesms.groups - -import androidx.annotation.VisibleForTesting -import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.utilities.Hex -import org.thoughtcrime.securesms.database.model.ThreadRecord -import org.thoughtcrime.securesms.dependencies.DatabaseComponent - -object OpenGroupMigrator { - const val HTTP_PREFIX = "__loki_public_chat_group__!687474703a2f2f" - private const val HTTPS_PREFIX = "__loki_public_chat_group__!68747470733a2f2f" - const val OPEN_GET_SESSION_TRAILING_DOT_ENCODED = "6f70656e2e67657473657373696f6e2e6f72672e" - const val LEGACY_GROUP_ENCODED_ID = "__loki_public_chat_group__!687474703a2f2f3131362e3230332e37302e33332e" // old IP based toByteArray() - const val NEW_GROUP_ENCODED_ID = "__loki_public_chat_group__!68747470733a2f2f6f70656e2e67657473657373696f6e2e6f72672e" // new URL based toByteArray() - - data class OpenGroupMapping(val stub: String, val legacyThreadId: Long, val newThreadId: Long?) - - @VisibleForTesting - fun Recipient.roomStub(): String? { - if (!isOpenGroupRecipient) return null - val serialized = address.serialize() - if (serialized.startsWith(LEGACY_GROUP_ENCODED_ID)) { - return serialized.replace(LEGACY_GROUP_ENCODED_ID,"") - } else if (serialized.startsWith(NEW_GROUP_ENCODED_ID)) { - return serialized.replace(NEW_GROUP_ENCODED_ID,"") - } else if (serialized.startsWith(HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED)) { - return serialized.replace(HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED, "") - } - return null - } - - @VisibleForTesting - fun getExistingMappings(legacy: List, new: List): List { - val legacyStubsMapping = legacy.mapNotNull { thread -> - val stub = thread.recipient.roomStub() - stub?.let { it to thread.threadId } - } - val newStubsMapping = new.mapNotNull { thread -> - val stub = thread.recipient.roomStub() - stub?.let { it to thread.threadId } - } - return legacyStubsMapping.map { (legacyEncodedStub, legacyId) -> - // get 'new' open group thread ID if stubs match - OpenGroupMapping( - legacyEncodedStub, - legacyId, - newStubsMapping.firstOrNull { (newEncodedStub, _) -> newEncodedStub == legacyEncodedStub }?.second - ) - } - } - - @JvmStatic - fun migrate(databaseComponent: DatabaseComponent) { - // migrate thread db - val threadDb = databaseComponent.threadDatabase() - - val legacyOpenGroups = threadDb.legacyOxenOpenGroups - val httpBasedNewGroups = threadDb.httpOxenOpenGroups - if (legacyOpenGroups.isEmpty() && httpBasedNewGroups.isEmpty()) return // no need to migrate - - val newOpenGroups = threadDb.httpsOxenOpenGroups - val firstStepMigration = getExistingMappings(legacyOpenGroups, newOpenGroups) - - val secondStepMigration = getExistingMappings(httpBasedNewGroups, newOpenGroups) - - val groupDb = databaseComponent.groupDatabase() - val lokiApiDb = databaseComponent.lokiAPIDatabase() - val smsDb = databaseComponent.smsDatabase() - val mmsDb = databaseComponent.mmsDatabase() - val lokiMessageDatabase = databaseComponent.lokiMessageDatabase() - val lokiThreadDatabase = databaseComponent.lokiThreadDatabase() - - firstStepMigration.forEach { (stub, old, new) -> - val legacyEncodedGroupId = LEGACY_GROUP_ENCODED_ID+stub - if (new == null) { - val newEncodedGroupId = NEW_GROUP_ENCODED_ID+stub - // migrate thread and group encoded values - threadDb.migrateEncodedGroup(old, newEncodedGroupId) - groupDb.migrateEncodedGroup(legacyEncodedGroupId, newEncodedGroupId) - // migrate Loki API DB values - // decode the hex to bytes, decode byte array to string i.e. "oxen" or "session" - val decodedStub = Hex.fromStringCondensed(stub).decodeToString() - val legacyLokiServerId = "${OpenGroupApi.legacyDefaultServer}.$decodedStub" - val newLokiServerId = "${OpenGroupApi.defaultServer}.$decodedStub" - lokiApiDb.migrateLegacyOpenGroup(legacyLokiServerId, newLokiServerId) - // migrate loki thread db server info - val oldServerInfo = lokiThreadDatabase.getOpenGroupChat(old) - val newServerInfo = oldServerInfo!!.copy(server = OpenGroupApi.defaultServer, id = newLokiServerId) - lokiThreadDatabase.setOpenGroupChat(newServerInfo, old) - } else { - // has a legacy and a new one - // migrate SMS and MMS tables - smsDb.migrateThreadId(old, new) - mmsDb.migrateThreadId(old, new) - lokiMessageDatabase.migrateThreadId(old, new) - // delete group for legacy ID - groupDb.delete(legacyEncodedGroupId) - // delete thread for legacy ID - threadDb.deleteConversation(old) - lokiThreadDatabase.removeOpenGroupChat(old) - } - // maybe migrate jobs here - } - - secondStepMigration.forEach { (stub, old, new) -> - val legacyEncodedGroupId = HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED + stub - if (new == null) { - val newEncodedGroupId = NEW_GROUP_ENCODED_ID+stub - // migrate thread and group encoded values - threadDb.migrateEncodedGroup(old, newEncodedGroupId) - groupDb.migrateEncodedGroup(legacyEncodedGroupId, newEncodedGroupId) - // migrate Loki API DB values - // decode the hex to bytes, decode byte array to string i.e. "oxen" or "session" - val decodedStub = Hex.fromStringCondensed(stub).decodeToString() - val legacyLokiServerId = "${OpenGroupApi.httpDefaultServer}.$decodedStub" - val newLokiServerId = "${OpenGroupApi.defaultServer}.$decodedStub" - lokiApiDb.migrateLegacyOpenGroup(legacyLokiServerId, newLokiServerId) - // migrate loki thread db server info - val oldServerInfo = lokiThreadDatabase.getOpenGroupChat(old) - val newServerInfo = oldServerInfo!!.copy(server = OpenGroupApi.defaultServer, id = newLokiServerId) - lokiThreadDatabase.setOpenGroupChat(newServerInfo, old) - } else { - // has a legacy and a new one - // migrate SMS and MMS tables - smsDb.migrateThreadId(old, new) - mmsDb.migrateThreadId(old, new) - lokiMessageDatabase.migrateThreadId(old, new) - // delete group for legacy ID - groupDb.delete(legacyEncodedGroupId) - // delete thread for legacy ID - threadDb.deleteConversation(old) - lokiThreadDatabase.removeOpenGroupChat(old) - } - // maybe migrate jobs here - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt index 0b3c44a54..702bf3392 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt @@ -7,10 +7,15 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.UiModeUtilities +import org.thoughtcrime.securesms.util.getConversationUnread +import javax.inject.Inject +@AndroidEntryPoint class ConversationOptionsBottomSheet(private val parentContext: Context) : BottomSheetDialogFragment(), View.OnClickListener { private lateinit var binding: FragmentConversationBottomSheetBinding //FIXME AC: Supplying a threadRecord directly into the field from an activity @@ -19,7 +24,10 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto // if we want to use dialog fragments properly. lateinit var thread: ThreadRecord + @Inject lateinit var configFactory: ConfigFactory + var onViewDetailsTapped: (() -> Unit?)? = null + var onCopyConversationId: (() -> Unit?)? = null var onPinTapped: (() -> Unit)? = null var onUnpinTapped: (() -> Unit)? = null var onBlockTapped: (() -> Unit)? = null @@ -37,6 +45,8 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto override fun onClick(v: View?) { when (v) { binding.detailsTextView -> onViewDetailsTapped?.invoke() + binding.copyConversationId -> onCopyConversationId?.invoke() + binding.copyCommunityUrl -> onCopyConversationId?.invoke() binding.pinTextView -> onPinTapped?.invoke() binding.unpinTextView -> onUnpinTapped?.invoke() binding.blockTextView -> onBlockTapped?.invoke() @@ -63,6 +73,10 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto } else { binding.detailsTextView.visibility = View.GONE } + binding.copyConversationId.visibility = if (!recipient.isGroupRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE + binding.copyConversationId.setOnClickListener(this) + binding.copyCommunityUrl.visibility = if (recipient.isOpenGroupRecipient) View.VISIBLE else View.GONE + binding.copyCommunityUrl.setOnClickListener(this) binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber binding.unMuteNotificationsTextView.setOnClickListener(this) @@ -70,7 +84,7 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted binding.notificationsTextView.setOnClickListener(this) binding.deleteTextView.setOnClickListener(this) - binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 + binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 || configFactory.convoVolatile?.getConversationUnread(thread) == true binding.markAllAsReadTextView.setOnClickListener(this) binding.pinTextView.isVisible = !thread.isPinned binding.unpinTextView.isVisible = thread.isPinned @@ -81,7 +95,6 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto 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/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index c6a6e1f7f..31b281c6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -6,12 +6,12 @@ import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.util.AttributeSet import android.util.TypedValue -import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.core.content.ContextCompat 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.ViewConversationBinding import org.session.libsession.utilities.recipients.Recipient @@ -19,12 +19,19 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.hig import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_ALL import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.getAccentColor +import org.thoughtcrime.securesms.util.getConversationUnread import java.util.Locale +import javax.inject.Inject +@AndroidEntryPoint class ConversationView : LinearLayout { + + @Inject lateinit var configFactory: ConfigFactory + private val binding: ViewConversationBinding by lazy { ViewConversationBinding.bind(this) } private val screenWidth = Resources.getSystem().displayMetrics.widthPixels var thread: ThreadRecord? = null @@ -58,7 +65,6 @@ class ConversationView : LinearLayout { } else { ContextCompat.getDrawable(context, R.drawable.conversation_view_background) } - binding.profilePictureView.root.glide = glide val unreadCount = thread.unreadCount if (thread.recipient.isBlocked) { binding.accentView.setBackgroundResource(R.color.destructive) @@ -71,7 +77,7 @@ class ConversationView : LinearLayout { // This would also not trigger the disappearing message timer which may or may not be desirable binding.accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE } - val formattedUnreadCount = if (thread.isRead) { + val formattedUnreadCount = if (unreadCount == 0) { null } else { if (unreadCount < 10000) unreadCount.toString() else "9999+" @@ -80,6 +86,7 @@ class ConversationView : LinearLayout { val textSize = if (unreadCount < 1000) 12.0f else 10.0f binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) + || (configFactory.convoVolatile?.getConversationUnread(thread) == true) binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup) val senderDisplayName = getUserDisplayName(thread.recipient) @@ -117,18 +124,18 @@ class ConversationView : LinearLayout { thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check) else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check) } - binding.profilePictureView.root.update(thread.recipient) + binding.profilePictureView.update(thread.recipient) } fun recycle() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() } private fun getUserDisplayName(recipient: Recipient): String? { return if (recipient.isLocalNumber) { context.getString(R.string.note_to_self) } else { - recipient.name // Internally uses the Contact API + recipient.toShortString() // Internally uses the Contact API } } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index f1a9c8ed9..59f324b52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -1,6 +1,10 @@ package org.thoughtcrime.securesms.home +import android.Manifest +import android.app.NotificationManager import android.content.BroadcastReceiver +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -8,10 +12,11 @@ import android.os.Bundle import android.text.SpannableString import android.widget.Toast import androidx.activity.viewModels -import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -25,11 +30,14 @@ import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.databinding.ActivityHomeBinding import network.loki.messenger.databinding.ViewMessageRequestBannerBinding +import network.loki.messenger.libsession_util.ConfigBase import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.JobQueue 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.GroupUtil import org.session.libsession.utilities.ProfilePictureModifiedEvent @@ -39,7 +47,6 @@ import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.MuteDialog import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.start.NewConversationFragment import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 @@ -48,8 +55,10 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter @@ -58,9 +67,13 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.notifications.PushRegistry import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate +import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.preferences.SettingsActivity +import org.thoughtcrime.securesms.showSessionDialog +import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.IP2Country @@ -78,6 +91,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), SeedReminderViewDelegate, GlobalSearchInputLayout.GlobalSearchInputLayoutListener { + companion object { + const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING" + } + + private lateinit var binding: ActivityHomeBinding private lateinit var glide: GlideRequests private var broadcastReceiver: BroadcastReceiver? = null @@ -85,8 +103,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var mmsSmsDatabase: MmsSmsDatabase @Inject lateinit var recipientDatabase: RecipientDatabase + @Inject lateinit var storage: Storage @Inject lateinit var groupDatabase: GroupDatabase @Inject lateinit var textSecurePreferences: TextSecurePreferences + @Inject lateinit var configFactory: ConfigFactory + @Inject lateinit var pushRegistry: PushRegistry private val globalSearchViewModel by viewModels() private val homeViewModel by viewModels() @@ -95,7 +116,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), get() = textSecurePreferences.getLocalNumber()!! private val homeAdapter: HomeAdapter by lazy { - HomeAdapter(context = this, listener = this) + HomeAdapter(context = this, configFactory = configFactory, listener = this) } private val globalSearchAdapter = GlobalSearchAdapter { model -> @@ -149,22 +170,23 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), // Set up Glide glide = GlideApp.with(this) // Set up toolbar buttons - binding.profileButton.root.glide = glide - binding.profileButton.root.setOnClickListener { openSettings() } + binding.profileButton.setOnClickListener { openSettings() } binding.searchViewContainer.setOnClickListener { binding.globalSearchInputLayout.requestFocus() } binding.sessionToolbar.disableClipping() // Set up seed reminder view - val hasViewedSeed = textSecurePreferences.getHasViewedSeed() - if (!hasViewedSeed) { - binding.seedReminderView.isVisible = true - binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated - binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) - binding.seedReminderView.setProgress(80, false) - binding.seedReminderView.delegate = this@HomeActivity - } else { - binding.seedReminderView.isVisible = false + lifecycleScope.launchWhenStarted { + val hasViewedSeed = textSecurePreferences.getHasViewedSeed() + if (!hasViewedSeed) { + binding.seedReminderView.isVisible = true + binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated + binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) + binding.seedReminderView.setProgress(80, false) + binding.seedReminderView.delegate = this@HomeActivity + } else { + binding.seedReminderView.isVisible = false + } } setupMessageRequestsBanner() // Set up recycler view @@ -174,6 +196,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), binding.recyclerView.adapter = homeAdapter binding.globalSearchRecycler.adapter = globalSearchAdapter + binding.configOutdatedView.setOnClickListener { + textSecurePreferences.setHasLegacyConfig(false) + updateLegacyConfigView() + } + // Set up empty state view binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() } IP2Country.configureIfNeeded(this@HomeActivity) @@ -190,14 +217,22 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), this.broadcastReceiver = broadcastReceiver LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged")) + // subscribe to outdated config updates, this should be removed after long enough time for device migration + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + TextSecurePreferences.events.filter { it == TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG }.collect { + updateLegacyConfigView() + } + } + } + lifecycleScope.launchWhenStarted { launch(Dispatchers.IO) { // Double check that the long poller is up (applicationContext as ApplicationContext).startPollingIfNeeded() // update things based on TextSecurePrefs (profile info etc) // Set up remaining components if needed - val application = ApplicationContext.getInstance(this@HomeActivity) - application.registerForFCMIfNeeded(false) + pushRegistry.refresh(false) if (textSecurePreferences.getLocalNumber() != null) { OpenGroupManager.startPolling() JobQueue.shared.resumePendingJobs() @@ -210,6 +245,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } } + // monitor the global search VM query launch { binding.globalSearchInputLayout.query @@ -262,6 +298,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } EventBus.getDefault().register(this@HomeActivity) + if (intent.hasExtra(FROM_ONBOARDING) + && intent.getBooleanExtra(FROM_ONBOARDING, false) + && !(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled() + ) { + Permissions.with(this) + .request(Manifest.permission.POST_NOTIFICATIONS) + .execute() + } } override fun onInputFocusChanged(hasFocus: Boolean) { @@ -310,16 +354,26 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } + private fun updateLegacyConfigView() { + binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset) + && textSecurePreferences.getHasLegacyConfig() + } + override fun onResume() { super.onResume() ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true) if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared IdentityKeyUtil.checkUpdate(this) - binding.profileButton.root.recycle() // clear cached image before update tje profilePictureView - binding.profileButton.root.update() + binding.profileButton.recycle() // clear cached image before update tje profilePictureView + binding.profileButton.update() if (textSecurePreferences.getHasViewedSeed()) { binding.seedReminderView.isVisible = false } + + updateLegacyConfigView() + + // TODO: remove this after enough updates that we can rely on ConfigBase.isNewConfigEnabled to always return true + // This will only run if we aren't using new configs, as they are schedule to sync when there are changes applied if (textSecurePreferences.getConfigurationMessageSynced()) { lifecycleScope.launch(Dispatchers.IO) { ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity) @@ -386,10 +440,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } private fun updateProfileButton() { - binding.profileButton.root.publicKey = publicKey - binding.profileButton.root.displayName = textSecurePreferences.getProfileName() - binding.profileButton.root.recycle() - binding.profileButton.root.update() + binding.profileButton.publicKey = publicKey + binding.profileButton.displayName = textSecurePreferences.getProfileName() + binding.profileButton.recycle() + binding.profileButton.update() } // endregion @@ -426,6 +480,24 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), userDetailsBottomSheet.arguments = bundle userDetailsBottomSheet.show(supportFragmentManager, userDetailsBottomSheet.tag) } + bottomSheet.onCopyConversationId = onCopyConversationId@{ + bottomSheet.dismiss() + if (!thread.recipient.isGroupRecipient && !thread.recipient.isLocalNumber) { + val clip = ClipData.newPlainText("Session ID", thread.recipient.address.toString()) + val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(clip) + Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + } + else if (thread.recipient.isOpenGroupRecipient) { + val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit + val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit + + 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() + } + } bottomSheet.onBlockTapped = { bottomSheet.dismiss() if (!thread.recipient.isBlocked) { @@ -468,35 +540,37 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } private fun blockConversation(thread: ThreadRecord) { - AlertDialog.Builder(this) - .setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question) - .setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ -> - lifecycleScope.launch(Dispatchers.IO) { - recipientDatabase.setBlocked(thread.recipient, true) - withContext(Dispatchers.Main) { - binding.recyclerView.adapter!!.notifyDataSetChanged() - dialog.dismiss() - } + showSessionDialog { + title(R.string.RecipientPreferenceActivity_block_this_contact_question) + text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact) + button(R.string.RecipientPreferenceActivity_block) { + lifecycleScope.launch(Dispatchers.IO) { + storage.setBlocked(listOf(thread.recipient), true) + + withContext(Dispatchers.Main) { + binding.recyclerView.adapter!!.notifyDataSetChanged() } - }.show() + } + } + cancelButton() + } } private fun unblockConversation(thread: ThreadRecord) { - AlertDialog.Builder(this) - .setTitle(R.string.RecipientPreferenceActivity_unblock_this_contact_question) - .setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ -> - lifecycleScope.launch(Dispatchers.IO) { - recipientDatabase.setBlocked(thread.recipient, false) - withContext(Dispatchers.Main) { - binding.recyclerView.adapter!!.notifyDataSetChanged() - dialog.dismiss() - } + showSessionDialog { + title(R.string.RecipientPreferenceActivity_unblock_this_contact_question) + text(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) + button(R.string.RecipientPreferenceActivity_unblock) { + lifecycleScope.launch(Dispatchers.IO) { + storage.setBlocked(listOf(thread.recipient), false) + + withContext(Dispatchers.Main) { + binding.recyclerView.adapter!!.notifyDataSetChanged() } - }.show() + } + } + cancelButton() + } } private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) { @@ -508,7 +582,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } } else { - MuteDialog.show(this) { until: Long -> + showMuteDialog(this) { until -> lifecycleScope.launch(Dispatchers.IO) { recipientDatabase.setMuted(thread.recipient, until) withContext(Dispatchers.Main) { @@ -530,14 +604,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), private fun setConversationPinned(threadId: Long, pinned: Boolean) { lifecycleScope.launch(Dispatchers.IO) { - threadDb.setPinned(threadId, pinned) + storage.setPinned(threadId, pinned) homeViewModel.tryUpdateChannel() } } private fun markAllAsRead(thread: ThreadRecord) { ThreadUtils.queue { - threadDb.markAllAsRead(thread.threadId, thread.recipient.isOpenGroupRecipient) + MessagingModuleConfiguration.shared.storage.markConversationAsRead(thread.threadId, SnodeAPI.nowWithOffset) } } @@ -554,48 +628,41 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } else { resources.getString(R.string.activity_home_delete_conversation_dialog_message) } - val dialog = AlertDialog.Builder(this) - dialog.setMessage(message) - dialog.setPositiveButton(R.string.yes) { _, _ -> - lifecycleScope.launch(Dispatchers.Main) { - val context = this@HomeActivity as Context - // Cancel any outstanding jobs - DatabaseComponent.get(context).sessionJobDatabase().cancelPendingMessageSendJobs(threadID) - // Send a leave group message if this is an active closed group - if (recipient.address.isClosedGroup && DatabaseComponent.get(context).groupDatabase().isActive(recipient.address.toGroupString())) { - var isClosedGroup: Boolean - var groupPublicKey: String? - try { - groupPublicKey = GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString() - isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey) - } catch (e: IOException) { - groupPublicKey = null - isClosedGroup = false + + showSessionDialog { + text(message) + button(R.string.yes) { + lifecycleScope.launch(Dispatchers.Main) { + val context = this@HomeActivity + // Cancel any outstanding jobs + DatabaseComponent.get(context).sessionJobDatabase().cancelPendingMessageSendJobs(threadID) + // Send a leave group message if this is an active closed group + if (recipient.address.isClosedGroup && DatabaseComponent.get(context).groupDatabase().isActive(recipient.address.toGroupString())) { + try { + GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString() + .takeIf(DatabaseComponent.get(context).lokiAPIDatabase()::isClosedGroup) + ?.let { MessageSender.explicitLeave(it, false) } + } catch (_: IOException) { + } } - if (isClosedGroup) { - MessageSender.explicitLeave(groupPublicKey!!, false) + // Delete the conversation + val v2OpenGroup = DatabaseComponent.get(context).lokiThreadDatabase().getOpenGroupChat(threadID) + if (v2OpenGroup != null) { + v2OpenGroup.apply { OpenGroupManager.delete(server, room, context) } + } else { + lifecycleScope.launch(Dispatchers.IO) { + threadDb.deleteConversation(threadID) + } } + // Update the badge count + ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) + // Notify the user + val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message + Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show() } - // Delete the conversation - val v2OpenGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadID) - if (v2OpenGroup != null) { - OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity) - } else { - lifecycleScope.launch(Dispatchers.IO) { - threadDb.deleteConversation(threadID) - } - } - // Update the badge count - ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) - // Notify the user - val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show() } + button(R.string.no) } - dialog.setNegativeButton(R.string.no) { _, _ -> - // Do nothing - } - dialog.create().show() } private fun openSettings() { @@ -609,17 +676,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } private fun hideMessageRequests() { - AlertDialog.Builder(this) - .setMessage("Hide message requests?") - .setPositiveButton(R.string.yes) { _, _ -> + showSessionDialog { + text("Hide message requests?") + button(R.string.yes) { textSecurePreferences.setHasHiddenMessageRequests() setupMessageRequestsBanner() homeViewModel.tryUpdateChannel() } - .setNegativeButton(R.string.no) { _, _ -> - // Do nothing - } - .create().show() + button(R.string.no) + } } private fun showNewConversation() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index 4273794f5..eaf242aae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -10,10 +10,12 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID import network.loki.messenger.R import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.mms.GlideRequests class HomeAdapter( private val context: Context, + private val configFactory: ConfigFactory, private val listener: ConversationClickListener ) : RecyclerView.Adapter(), ListUpdateCallback { @@ -29,7 +31,7 @@ class HomeAdapter( get() = _data.toList() set(newData) { val previousData = _data.toList() - val diff = HomeDiffUtil(previousData, newData, context) + val diff = HomeDiffUtil(previousData, newData, context, configFactory) val diffResult = DiffUtil.calculateDiff(diff) _data = newData diffResult.dispatchUpdatesTo(this as ListUpdateCallback) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt index 1baec2085..0fe93d41d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt @@ -3,11 +3,14 @@ package org.thoughtcrime.securesms.home import android.content.Context import androidx.recyclerview.widget.DiffUtil import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.util.getConversationUnread class HomeDiffUtil( private val old: List, private val new: List, - private val context: Context + private val context: Context, + private val configFactory: ConfigFactory ): DiffUtil.Callback() { override fun getOldListSize(): Int = old.size @@ -28,8 +31,11 @@ class HomeDiffUtil( if (isSameItem) { isSameItem = (oldItem.unreadCount == newItem.unreadCount) } if (isSameItem) { isSameItem = (oldItem.isPinned == newItem.isPinned) } - // Note: For some reason the 'hashCode' value can change after initialisation so we can't cache it - if (isSameItem) { isSameItem = (oldItem.recipient.hashCode() == newItem.recipient.hashCode()) } + // The recipient is passed as a reference and changes to recipients update the reference so we + // need to cache the hashCode for the recipient and use that for diffing - unfortunately + // recipient data is also loaded asyncronously which means every thread will refresh at least + // once when the initial recipient data is loaded + if (isSameItem) { isSameItem = (oldItem.initialRecipientHash == newItem.initialRecipientHash) } // Note: Two instances of 'SpannableString' may not equate even though their content matches if (isSameItem) { isSameItem = (oldItem.getDisplayBody(context).toString() == newItem.getDisplayBody(context).toString()) } @@ -39,7 +45,9 @@ class HomeDiffUtil( oldItem.isFailed == newItem.isFailed && oldItem.isDelivered == newItem.isDelivered && oldItem.isSent == newItem.isSent && - oldItem.isPending == newItem.isPending + oldItem.isPending == newItem.isPending && + oldItem.lastSeen == newItem.lastSeen && + configFactory.convoVolatile?.getConversationUnread(newItem) != true ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt index 947bd89b4..7ab7bfb50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt @@ -9,9 +9,14 @@ import android.graphics.Paint import android.util.AttributeSet import android.view.View import androidx.annotation.ColorInt +import androidx.lifecycle.coroutineScope import androidx.localbroadcastmanager.content.LocalBroadcastManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.withContext import network.loki.messenger.R import org.session.libsession.snode.OnionRequestAPI +import org.thoughtcrime.securesms.conversation.v2.ViewUtil import org.thoughtcrime.securesms.util.getColorWithID import org.thoughtcrime.securesms.util.toPx @@ -29,6 +34,8 @@ class PathStatusView : View { result } + private var updateJob: Job? = null + constructor(context: Context) : super(context) { initialize() } @@ -87,16 +94,21 @@ class PathStatusView : View { private fun handlePathsBuiltEvent() { update() } private fun update() { - if (OnionRequestAPI.paths.isNotEmpty()) { - setBackgroundResource(R.drawable.accent_dot) - val hasPathsColor = context.getColor(R.color.accent_green) - mainColor = hasPathsColor - sessionShadowColor = hasPathsColor - } else { - setBackgroundResource(R.drawable.paths_building_dot) - val pathsBuildingColor = resources.getColorWithID(R.color.paths_building, context.theme) - mainColor = pathsBuildingColor - sessionShadowColor = pathsBuildingColor + if (updateJob?.isActive != true) { // false or null + updateJob = ViewUtil.getActivityLifecycle(this)?.coroutineScope?.launchWhenStarted { + val paths = withContext(Dispatchers.IO) { OnionRequestAPI.paths } + if (paths.isNotEmpty()) { + setBackgroundResource(R.drawable.accent_dot) + val hasPathsColor = context.getColor(R.color.accent_green) + mainColor = hasPathsColor + sessionShadowColor = hasPathsColor + } else { + setBackgroundResource(R.drawable.paths_building_dot) + val pathsBuildingColor = resources.getColorWithID(R.color.paths_building, context.theme) + mainColor = pathsBuildingColor + sessionShadowColor = pathsBuildingColor + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt index f3915abff..ad8f2d042 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt @@ -25,9 +25,7 @@ import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.GlideApp -import org.thoughtcrime.securesms.util.UiModeUtilities import javax.inject.Inject @AndroidEntryPoint @@ -55,12 +53,12 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false) val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss() with(binding) { - profilePictureView.root.publicKey = publicKey - profilePictureView.root.glide = GlideApp.with(this@UserDetailsBottomSheet) - profilePictureView.root.isLarge = true - profilePictureView.root.update(recipient) + profilePictureView.publicKey = publicKey + profilePictureView.isLarge = true + profilePictureView.update(recipient) nameTextViewContainer.visibility = View.VISIBLE nameTextViewContainer.setOnClickListener { + if (recipient.isOpenGroupInboxRecipient || recipient.isOpenGroupOutboxRecipient) return@setOnClickListener nameTextViewContainer.visibility = View.INVISIBLE nameEditTextContainer.visibility = View.VISIBLE nicknameEditText.text = null @@ -87,8 +85,14 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { } nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally - publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient && !threadRecipient.isOpenGroupInboxRecipient - messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey) == IdPrefix.BLINDED + nameEditIcon.isVisible = threadRecipient.isContactRecipient + && !threadRecipient.isOpenGroupInboxRecipient + && !threadRecipient.isOpenGroupOutboxRecipient + + publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient + && !threadRecipient.isOpenGroupInboxRecipient + && !threadRecipient.isOpenGroupOutboxRecipient + messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true publicKeyTextView.text = publicKey publicKeyTextView.setOnLongClickListener { val clipboard = @@ -117,8 +121,7 @@ class UserDetailsBottomSheet: 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) } fun saveNickName(recipient: Recipient) = with(binding) { @@ -131,10 +134,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { newNickName = nicknameEditText.text.toString() } val publicKey = recipient.address.serialize() - val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase() - val contact = contactDB.getContactWithSessionID(publicKey) ?: Contact(publicKey) + val storage = MessagingModuleConfiguration.shared.storage + val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey) contact.nickname = newNickName - contactDB.setContact(contact) + storage.setContact(contact) nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt index fab8bca99..7cf953be2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt @@ -83,22 +83,20 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi override fun onViewRecycled(holder: RecyclerView.ViewHolder) { if (holder is ContentView) { - holder.binding.searchResultProfilePicture.root.recycle() + holder.binding.searchResultProfilePicture.recycle() } } class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) { - val binding = ViewGlobalSearchResultBinding.bind(view).apply { - searchResultProfilePicture.root.glide = GlideApp.with(root) - } + val binding = ViewGlobalSearchResultBinding.bind(view) fun bindPayload(newQuery: String, model: Model) { bindQuery(newQuery, model) } fun bind(query: String, model: Model) { - binding.searchResultProfilePicture.root.recycle() + binding.searchResultProfilePicture.recycle() when (model) { is Model.GroupConversation -> bindModel(query, model) is Model.Contact -> bindModel(query, model) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt index 2c64ded86..5371bb71c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt @@ -12,6 +12,7 @@ import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation +import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages import org.thoughtcrime.securesms.util.DateUtils @@ -76,6 +77,8 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) { } binding.searchResultSubtitle.text = getHighlight(query, membersString) } + is Header, // do nothing for header + is SavedMessages -> Unit // do nothing for saved messages (displays note to self) } } @@ -84,12 +87,12 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? { } fun ContentView.bindModel(query: String?, model: GroupConversation) { - binding.searchResultProfilePicture.root.isVisible = true + binding.searchResultProfilePicture.isVisible = true binding.searchResultSavedMessages.isVisible = false binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup binding.searchResultTimestamp.isVisible = false val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false) - binding.searchResultProfilePicture.root.update(threadRecipient) + binding.searchResultProfilePicture.update(threadRecipient) val nameString = model.groupRecord.title binding.searchResultTitle.text = getHighlight(query, nameString) @@ -105,14 +108,14 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) { } fun ContentView.bindModel(query: String?, model: ContactModel) { - binding.searchResultProfilePicture.root.isVisible = true + binding.searchResultProfilePicture.isVisible = true binding.searchResultSavedMessages.isVisible = false binding.searchResultSubtitle.isVisible = false binding.searchResultTimestamp.isVisible = false binding.searchResultSubtitle.text = null val recipient = Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false) - binding.searchResultProfilePicture.root.update(recipient) + binding.searchResultProfilePicture.update(recipient) val nameString = model.contact.getSearchName() binding.searchResultTitle.text = getHighlight(query, nameString) } @@ -121,12 +124,12 @@ fun ContentView.bindModel(model: SavedMessages) { binding.searchResultSubtitle.isVisible = false binding.searchResultTimestamp.isVisible = false binding.searchResultTitle.setText(R.string.note_to_self) - binding.searchResultProfilePicture.root.isVisible = false + binding.searchResultProfilePicture.isVisible = false binding.searchResultSavedMessages.isVisible = true } fun ContentView.bindModel(query: String?, model: Message) { - binding.searchResultProfilePicture.root.isVisible = true + binding.searchResultProfilePicture.isVisible = true binding.searchResultSavedMessages.isVisible = false binding.searchResultTimestamp.isVisible = true // val hasUnreads = model.unread > 0 @@ -135,7 +138,7 @@ fun ContentView.bindModel(query: String?, model: Message) { // binding.unreadCountTextView.text = model.unread.toString() // } binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs) - binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient) + binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient) val textSpannable = SpannableStringBuilder() if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { // group chat, bind diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java deleted file mode 100644 index dc1d2afcf..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import android.app.AlarmManager; -import android.app.Application; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import androidx.annotation.NonNull; - -import com.annimon.stream.Stream; - -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.ApplicationContext; - -import java.util.List; -import java.util.UUID; - -import network.loki.messenger.BuildConfig; - -/** - * Schedules tasks using the {@link AlarmManager}. - * - * Given that this scheduler is only used when {@link KeepAliveService} is also used (which keeps - * all of the {@link ConstraintObserver}s running), this only needs to schedule future runs in - * situations where all constraints are already met. Otherwise, the {@link ConstraintObserver}s will - * trigger future runs when the constraints are met. - * - * For the same reason, this class also doesn't have to schedule jobs that don't have delays. - * - * Important: Only use on API < 26. - */ -public class AlarmManagerScheduler implements Scheduler { - - private static final String TAG = AlarmManagerScheduler.class.getSimpleName(); - - private final Application application; - - AlarmManagerScheduler(@NonNull Application application) { - this.application = application; - } - - @Override - public void schedule(long delay, @NonNull List constraints) { - if (delay > 0 && Stream.of(constraints).allMatch(Constraint::isMet)) { - setUniqueAlarm(application, System.currentTimeMillis() + delay); - } - } - - private void setUniqueAlarm(@NonNull Context context, long time) { - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(context, RetryReceiver.class); - - intent.setAction(BuildConfig.APPLICATION_ID + UUID.randomUUID().toString()); - alarmManager.set(AlarmManager.RTC_WAKEUP, time, PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)); - - Log.i(TAG, "Set an alarm to retry a job in " + (time - System.currentTimeMillis()) + " ms."); - } - - public static class RetryReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - Log.i(TAG, "Received an alarm to retry a job."); - ApplicationContext.getInstance(context).getJobManager().wakeUp(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/CompositeScheduler.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/CompositeScheduler.java deleted file mode 100644 index 322366f4f..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/CompositeScheduler.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import androidx.annotation.NonNull; - -import java.util.Arrays; -import java.util.List; - -class CompositeScheduler implements Scheduler { - - private final List schedulers; - - CompositeScheduler(@NonNull Scheduler... schedulers) { - this.schedulers = Arrays.asList(schedulers); - } - - @Override - public void schedule(long delay, @NonNull List constraints) { - for (Scheduler scheduler : schedulers) { - scheduler.schedule(delay, constraints); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ConstraintInstantiator.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ConstraintInstantiator.java deleted file mode 100644 index b0a67e3d1..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ConstraintInstantiator.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import androidx.annotation.NonNull; - -import java.util.HashMap; -import java.util.Map; - -public class ConstraintInstantiator { - - private final Map constraintFactories; - - ConstraintInstantiator(@NonNull Map constraintFactories) { - this.constraintFactories = new HashMap<>(constraintFactories); - } - - public @NonNull Constraint instantiate(@NonNull String constraintFactoryKey) { - if (constraintFactories.containsKey(constraintFactoryKey)) { - return constraintFactories.get(constraintFactoryKey).create(); - } else { - throw new IllegalStateException("Tried to instantiate a constraint with key '" + constraintFactoryKey + "', but no matching factory was found."); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ConstraintObserver.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ConstraintObserver.java deleted file mode 100644 index fd7f4fd43..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ConstraintObserver.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import androidx.annotation.NonNull; - -public interface ConstraintObserver { - - void register(@NonNull Notifier notifier); - - interface Notifier { - void onConstraintMet(@NonNull String reason); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/DependencyInjector.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/DependencyInjector.java deleted file mode 100644 index c8a266bd8..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/DependencyInjector.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (C) 2014 Open Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.jobmanager; - -/** - * Interface responsible for injecting dependencies into Jobs. - */ -public interface DependencyInjector { - void injectDependencies(Object object); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ExecutorFactory.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ExecutorFactory.java deleted file mode 100644 index b0c2b974d..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/ExecutorFactory.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import androidx.annotation.NonNull; - -import java.util.concurrent.ExecutorService; - -public interface ExecutorFactory { - @NonNull ExecutorService newSingleThreadExecutor(@NonNull String name); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/InAppScheduler.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/InAppScheduler.java deleted file mode 100644 index b0f314eaa..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/InAppScheduler.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import android.os.Handler; -import android.os.HandlerThread; -import androidx.annotation.NonNull; - -import com.annimon.stream.Stream; - -import org.session.libsignal.utilities.Log; - -import java.util.List; - -/** - * Schedules future runs on an in-app handler. Intended to be used in combination with a persistent - * {@link Scheduler} to improve responsiveness when the app is open. - * - * This should only schedule runs when all constraints are met. Because this only works when the - * app is foregrounded, jobs that don't have their constraints met will be run when the relevant - * {@link ConstraintObserver} is triggered. - * - * Similarly, this does not need to schedule retries with no delay, as this doesn't provide any - * persistence, and other mechanisms will take care of that. - */ -class InAppScheduler implements Scheduler { - - private static final String TAG = InAppScheduler.class.getSimpleName(); - - private final JobManager jobManager; - private final Handler handler; - - InAppScheduler(@NonNull JobManager jobManager) { - HandlerThread handlerThread = new HandlerThread("InAppScheduler"); - handlerThread.start(); - - this.jobManager = jobManager; - this.handler = new Handler(handlerThread.getLooper()); - } - - @Override - public void schedule(long delay, @NonNull List constraints) { - if (delay > 0 && Stream.of(constraints).allMatch(Constraint::isMet)) { - Log.i(TAG, "Scheduling a retry in " + delay + " ms."); - handler.postDelayed(() -> { - Log.i(TAG, "Triggering a job retry."); - jobManager.wakeUp(); - }, delay); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java deleted file mode 100644 index 990207779..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java +++ /dev/null @@ -1,286 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import org.session.libsession.messaging.utilities.Data; -import org.session.libsignal.utilities.Log; - -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -/** - * A durable unit of work. - * - * Jobs have {@link Parameters} that describe the conditions upon when you'd like them to run, how - * often they should be retried, and how long they should be retried for. - * - * Never rely on a specific instance of this class being run. It can be created and destroyed as the - * job is retried. State that you want to save is persisted to a {@link Data} object in - * {@link #serialize()}. Your job is then recreated using a {@link Factory} that you register in - * {@link JobManager.Configuration.Builder#setJobFactories(Map)}, which is given the saved - * {@link Data} bundle. - * - * @deprecated - * use WorkManager - * API instead. - */ -public abstract class Job { - - private static final String TAG = Log.tag(Job.class); - - private final Parameters parameters; - - private String id; - private int runAttempt; - private long nextRunAttemptTime; - - protected Context context; - - public Job(@NonNull Parameters parameters) { - this.parameters = parameters; - } - - public final String getId() { - return id; - } - - public final @NonNull Parameters getParameters() { - return parameters; - } - - public final int getRunAttempt() { - return runAttempt; - } - - public final long getNextRunAttemptTime() { - return nextRunAttemptTime; - } - - /** - * This is already called by {@link JobController} during job submission, but if you ever run a - * job without submitting it to the {@link JobManager}, then you'll need to invoke this yourself. - */ - public final void setContext(@NonNull Context context) { - this.context = context; - } - - /** Should only be invoked by {@link JobController} */ - final void setId(@NonNull String id) { - this.id = id; - } - - /** Should only be invoked by {@link JobController} */ - final void setRunAttempt(int runAttempt) { - this.runAttempt = runAttempt; - } - - /** Should only be invoked by {@link JobController} */ - final void setNextRunAttemptTime(long nextRunAttemptTime) { - this.nextRunAttemptTime = nextRunAttemptTime; - } - - @WorkerThread - final void onSubmit() { - Log.i(TAG, JobLogger.format(this, "onSubmit()")); - onAdded(); - } - - /** - * Called when the job is first submitted to the {@link JobManager}. - */ - @WorkerThread - public void onAdded() { - } - - /** - * Called after a job has run and its determined that a retry is required. - */ - @WorkerThread - public void onRetry() { - } - - /** - * Serialize your job state so that it can be recreated in the future. - */ - public abstract @NonNull Data serialize(); - - /** - * Returns the key that can be used to find the relevant factory needed to create your job. - */ - public abstract @NonNull String getFactoryKey(); - - /** - * Called to do your actual work. - */ - @WorkerThread - public abstract @NonNull Result run(); - - /** - * Called when your job has completely failed. - */ - @WorkerThread - public abstract void onCanceled(); - - public interface Factory { - @NonNull T create(@NonNull Parameters parameters, @NonNull Data data); - } - - public enum Result { - SUCCESS, FAILURE, RETRY - } - - public static final class Parameters { - - public static final int IMMORTAL = -1; - public static final int UNLIMITED = -1; - - private final long createTime; - private final long lifespan; - private final int maxAttempts; - private final long maxBackoff; - private final int maxInstances; - private final String queue; - private final List constraintKeys; - - private Parameters(long createTime, - long lifespan, - int maxAttempts, - long maxBackoff, - int maxInstances, - @Nullable String queue, - @NonNull List constraintKeys) - { - this.createTime = createTime; - this.lifespan = lifespan; - this.maxAttempts = maxAttempts; - this.maxBackoff = maxBackoff; - this.maxInstances = maxInstances; - this.queue = queue; - this.constraintKeys = constraintKeys; - } - - public long getCreateTime() { - return createTime; - } - - public long getLifespan() { - return lifespan; - } - - public int getMaxAttempts() { - return maxAttempts; - } - - public long getMaxBackoff() { - return maxBackoff; - } - - public int getMaxInstances() { - return maxInstances; - } - - public @Nullable String getQueue() { - return queue; - } - - public List getConstraintKeys() { - return constraintKeys; - } - - - public static final class Builder { - - private long createTime = System.currentTimeMillis(); - private long maxBackoff = TimeUnit.SECONDS.toMillis(30); - private long lifespan = IMMORTAL; - private int maxAttempts = 1; - private int maxInstances = UNLIMITED; - private String queue = null; - private List constraintKeys = new LinkedList<>(); - - /** Should only be invoked by {@link JobController} */ - Builder setCreateTime(long createTime) { - this.createTime = createTime; - return this; - } - - /** - * Specify the amount of time this job is allowed to be retried. Defaults to {@link #IMMORTAL}. - */ - public @NonNull Builder setLifespan(long lifespan) { - this.lifespan = lifespan; - return this; - } - - /** - * Specify the maximum number of times you want to attempt this job. Defaults to 1. - */ - public @NonNull Builder setMaxAttempts(int maxAttempts) { - this.maxAttempts = maxAttempts; - return this; - } - - /** - * Specify the longest amount of time to wait between retries. No guarantees that this will - * be respected on API >= 26. - */ - public @NonNull Builder setMaxBackoff(long maxBackoff) { - this.maxBackoff = maxBackoff; - return this; - } - - /** - * Specify the maximum number of instances you'd want of this job at any given time. If - * enqueueing this job would put it over that limit, it will be ignored. - * - * Duplicates are determined by two jobs having the same {@link Job#getFactoryKey()}. - * - * This property is ignored if the job is submitted as part of a {@link JobManager.Chain}. - * - * Defaults to {@link #UNLIMITED}. - */ - public @NonNull Builder setMaxInstances(int maxInstances) { - this.maxInstances = maxInstances; - return this; - } - - /** - * Specify a string representing a queue. All jobs within the same queue are run in a - * serialized fashion -- one after the other, in order of insertion. Failure of a job earlier - * in the queue has no impact on the execution of jobs later in the queue. - */ - public @NonNull Builder setQueue(@Nullable String queue) { - this.queue = queue; - return this; - } - - /** - * Add a constraint via the key that was used to register its factory in - * {@link JobManager.Configuration)}; - */ - public @NonNull Builder addConstraint(@NonNull String constraintKey) { - constraintKeys.add(constraintKey); - return this; - } - - /** - * Set constraints via the key that was used to register its factory in - * {@link JobManager.Configuration)}; - */ - public @NonNull Builder setConstraints(@NonNull List constraintKeys) { - this.constraintKeys.clear(); - this.constraintKeys.addAll(constraintKeys); - return this; - } - - public @NonNull Parameters build() { - return new Parameters(createTime, lifespan, maxAttempts, maxBackoff, maxInstances, queue, constraintKeys); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobController.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobController.java deleted file mode 100644 index 33345a03e..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobController.java +++ /dev/null @@ -1,354 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import android.app.Application; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import com.annimon.stream.Stream; - -import org.session.libsession.messaging.utilities.Data; -import org.session.libsession.utilities.Debouncer; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec; -import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.UUID; - -/** - * Manages the queue of jobs. This is the only class that should write to {@link JobStorage} to - * ensure consistency. - */ -class JobController { - - private static final String TAG = JobController.class.getSimpleName(); - - private final Application application; - private final JobStorage jobStorage; - private final JobInstantiator jobInstantiator; - private final ConstraintInstantiator constraintInstantiator; - private final Data.Serializer dataSerializer; - private final Scheduler scheduler; - private final Debouncer debouncer; - private final Callback callback; - private final Set runningJobs; - - JobController(@NonNull Application application, - @NonNull JobStorage jobStorage, - @NonNull JobInstantiator jobInstantiator, - @NonNull ConstraintInstantiator constraintInstantiator, - @NonNull Data.Serializer dataSerializer, - @NonNull Scheduler scheduler, - @NonNull Debouncer debouncer, - @NonNull Callback callback) - { - this.application = application; - this.jobStorage = jobStorage; - this.jobInstantiator = jobInstantiator; - this.constraintInstantiator = constraintInstantiator; - this.dataSerializer = dataSerializer; - this.scheduler = scheduler; - this.debouncer = debouncer; - this.callback = callback; - this.runningJobs = new HashSet<>(); - } - - @WorkerThread - synchronized void init() { - jobStorage.init(); - jobStorage.updateAllJobsToBePending(); - notifyAll(); - } - - synchronized void wakeUp() { - notifyAll(); - } - - @WorkerThread - synchronized void submitNewJobChain(@NonNull List> chain) { - chain = Stream.of(chain).filterNot(List::isEmpty).toList(); - - if (chain.isEmpty()) { - Log.w(TAG, "Tried to submit an empty job chain. Skipping."); - return; - } - - if (chainExceedsMaximumInstances(chain)) { - Job solo = chain.get(0).get(0); - Log.w(TAG, JobLogger.format(solo, "Already at the max instance count of " + solo.getParameters().getMaxInstances() + ". Skipping.")); - return; - } - - insertJobChain(chain); - scheduleJobs(chain.get(0)); - triggerOnSubmit(chain); - notifyAll(); - } - - @WorkerThread - synchronized void onRetry(@NonNull Job job) { - int nextRunAttempt = job.getRunAttempt() + 1; - long nextRunAttemptTime = calculateNextRunAttemptTime(System.currentTimeMillis(), nextRunAttempt, job.getParameters().getMaxBackoff()); - - jobStorage.updateJobAfterRetry(job.getId(), false, nextRunAttempt, nextRunAttemptTime); - - List constraints = Stream.of(jobStorage.getConstraintSpecs(job.getId())) - .map(ConstraintSpec::getFactoryKey) - .map(constraintInstantiator::instantiate) - .toList(); - - - long delay = Math.max(0, nextRunAttemptTime - System.currentTimeMillis()); - - Log.i(TAG, JobLogger.format(job, "Scheduling a retry in " + delay + " ms.")); - scheduler.schedule(delay, constraints); - - notifyAll(); - } - - synchronized void onJobFinished(@NonNull Job job) { - runningJobs.remove(job.getId()); - } - - @WorkerThread - synchronized void onSuccess(@NonNull Job job) { - jobStorage.deleteJob(job.getId()); - notifyAll(); - } - - /** - * @return The list of all dependent jobs that should also be failed. - */ - @WorkerThread - synchronized @NonNull List onFailure(@NonNull Job job) { - List dependents = Stream.of(jobStorage.getDependencySpecsThatDependOnJob(job.getId())) - .map(DependencySpec::getJobId) - .map(jobStorage::getJobSpec) - .withoutNulls() - .map(jobSpec -> { - List constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId()); - return createJob(jobSpec, constraintSpecs); - }) - .toList(); - - List all = new ArrayList<>(dependents.size() + 1); - all.add(job); - all.addAll(dependents); - - jobStorage.deleteJobs(Stream.of(all).map(Job::getId).toList()); - - return dependents; - } - - /** - * Retrieves the next job that is eligible for execution. To be 'eligible' means that the job: - * - Has no dependencies - * - Has no unmet constraints - * - * This method will block until a job is available. - * When the job returned from this method has been run, you must call {@link #onJobFinished(Job)}. - */ - @WorkerThread - synchronized @NonNull Job pullNextEligibleJobForExecution() { - try { - Job job; - - while ((job = getNextEligibleJobForExecution()) == null) { - if (runningJobs.isEmpty()) { - debouncer.publish(callback::onEmpty); - } - - wait(); - } - - jobStorage.updateJobRunningState(job.getId(), true); - runningJobs.add(job.getId()); - - return job; - } catch (InterruptedException e) { - Log.e(TAG, "Interrupted."); - throw new AssertionError(e); - } - } - - /** - * Retrieves a string representing the state of the job queue. Intended for debugging. - */ - @WorkerThread - synchronized @NonNull String getDebugInfo() { - List jobs = jobStorage.getAllJobSpecs(); - List constraints = jobStorage.getAllConstraintSpecs(); - List dependencies = jobStorage.getAllDependencySpecs(); - - StringBuilder info = new StringBuilder(); - - info.append("-- Jobs\n"); - if (!jobs.isEmpty()) { - Stream.of(jobs).forEach(j -> info.append(j.toString()).append('\n')); - } else { - info.append("None\n"); - } - - info.append("\n-- Constraints\n"); - if (!constraints.isEmpty()) { - Stream.of(constraints).forEach(c -> info.append(c.toString()).append('\n')); - } else { - info.append("None\n"); - } - - info.append("\n-- Dependencies\n"); - if (!dependencies.isEmpty()) { - Stream.of(dependencies).forEach(d -> info.append(d.toString()).append('\n')); - } else { - info.append("None\n"); - } - - return info.toString(); - } - - @WorkerThread - private boolean chainExceedsMaximumInstances(@NonNull List> chain) { - if (chain.size() == 1 && chain.get(0).size() == 1) { - Job solo = chain.get(0).get(0); - - if (solo.getParameters().getMaxInstances() != Job.Parameters.UNLIMITED && - jobStorage.getJobInstanceCount(solo.getFactoryKey()) >= solo.getParameters().getMaxInstances()) - { - return true; - } - } - return false; - } - - @WorkerThread - private void triggerOnSubmit(@NonNull List> chain) { - Stream.of(chain) - .forEach(list -> Stream.of(list).forEach(job -> { - job.setContext(application); - job.onSubmit(); - })); - } - - @WorkerThread - private void insertJobChain(@NonNull List> chain) { - List fullSpecs = new LinkedList<>(); - List dependsOn = Collections.emptyList(); - - for (List jobList : chain) { - for (Job job : jobList) { - fullSpecs.add(buildFullSpec(job, dependsOn)); - } - dependsOn = jobList; - } - - jobStorage.insertJobs(fullSpecs); - } - - @WorkerThread - private @NonNull FullSpec buildFullSpec(@NonNull Job job, @NonNull List dependsOn) { - String id = UUID.randomUUID().toString(); - - job.setId(id); - job.setRunAttempt(0); - - JobSpec jobSpec = new JobSpec(job.getId(), - job.getFactoryKey(), - job.getParameters().getQueue(), - job.getParameters().getCreateTime(), - job.getNextRunAttemptTime(), - job.getRunAttempt(), - job.getParameters().getMaxAttempts(), - job.getParameters().getMaxBackoff(), - job.getParameters().getLifespan(), - job.getParameters().getMaxInstances(), - dataSerializer.serialize(job.serialize()), - false); - - List constraintSpecs = Stream.of(job.getParameters().getConstraintKeys()) - .map(key -> new ConstraintSpec(jobSpec.getId(), key)) - .toList(); - - List dependencySpecs = Stream.of(dependsOn) - .map(depends -> new DependencySpec(job.getId(), depends.getId())) - .toList(); - - return new FullSpec(jobSpec, constraintSpecs, dependencySpecs); - } - - @WorkerThread - private void scheduleJobs(@NonNull List jobs) { - for (Job job : jobs) { - List constraints = Stream.of(job.getParameters().getConstraintKeys()) - .map(key -> new ConstraintSpec(job.getId(), key)) - .map(ConstraintSpec::getFactoryKey) - .map(constraintInstantiator::instantiate) - .toList(); - - scheduler.schedule(0, constraints); - } - } - - @WorkerThread - private @Nullable Job getNextEligibleJobForExecution() { - List jobSpecs = jobStorage.getPendingJobsWithNoDependenciesInCreatedOrder(System.currentTimeMillis()); - - for (JobSpec jobSpec : jobSpecs) { - List constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId()); - List constraints = Stream.of(constraintSpecs) - .map(ConstraintSpec::getFactoryKey) - .map(constraintInstantiator::instantiate) - .toList(); - - if (Stream.of(constraints).allMatch(Constraint::isMet)) { - return createJob(jobSpec, constraintSpecs); - } - } - - return null; - } - - private @NonNull Job createJob(@NonNull JobSpec jobSpec, @NonNull List constraintSpecs) { - Job.Parameters parameters = buildJobParameters(jobSpec, constraintSpecs); - Data data = dataSerializer.deserialize(jobSpec.getSerializedData()); - Job job = jobInstantiator.instantiate(jobSpec.getFactoryKey(), parameters, data); - - job.setId(jobSpec.getId()); - job.setRunAttempt(jobSpec.getRunAttempt()); - job.setNextRunAttemptTime(jobSpec.getNextRunAttemptTime()); - job.setContext(application); - - return job; - } - - private @NonNull Job.Parameters buildJobParameters(@NonNull JobSpec jobSpec, @NonNull List constraintSpecs) { - return new Job.Parameters.Builder() - .setCreateTime(jobSpec.getCreateTime()) - .setLifespan(jobSpec.getLifespan()) - .setMaxAttempts(jobSpec.getMaxAttempts()) - .setQueue(jobSpec.getQueueKey()) - .setConstraints(Stream.of(constraintSpecs).map(ConstraintSpec::getFactoryKey).toList()) - .build(); - } - - private long calculateNextRunAttemptTime(long currentTime, int nextAttempt, long maxBackoff) { - int boundedAttempt = Math.min(nextAttempt, 30); - long exponentialBackoff = (long) Math.pow(2, boundedAttempt) * 1000; - long actualBackoff = Math.min(exponentialBackoff, maxBackoff); - - return currentTime + actualBackoff; - } - - interface Callback { - void onEmpty(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobInstantiator.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobInstantiator.java deleted file mode 100644 index 6d1527d13..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobInstantiator.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.utilities.Data; - -import java.util.HashMap; -import java.util.Map; - -class JobInstantiator { - - private final Map jobFactories; - - JobInstantiator(@NonNull Map jobFactories) { - this.jobFactories = new HashMap<>(jobFactories); - } - - public @NonNull Job instantiate(@NonNull String jobFactoryKey, @NonNull Job.Parameters parameters, @NonNull Data data) { - if (jobFactories.containsKey(jobFactoryKey)) { - return jobFactories.get(jobFactoryKey).create(parameters, data); - } else { - throw new IllegalStateException("Tried to instantiate a job with key '" + jobFactoryKey + "', but no matching factory was found."); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobLogger.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobLogger.java deleted file mode 100644 index c35f6dc1a..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobLogger.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import androidx.annotation.NonNull; -import android.text.TextUtils; - -public class JobLogger { - - public static String format(@NonNull Job job, @NonNull String event) { - return format(job, "", event); - } - - public static String format(@NonNull Job job, @NonNull String extraTag, @NonNull String event) { - String id = job.getId(); - String tag = TextUtils.isEmpty(extraTag) ? "" : "[" + extraTag + "]"; - long timeSinceSubmission = System.currentTimeMillis() - job.getParameters().getCreateTime(); - int runAttempt = job.getRunAttempt() + 1; - String maxAttempts = job.getParameters().getMaxAttempts() == Job.Parameters.UNLIMITED ? "Unlimited" - : String.valueOf(job.getParameters().getMaxAttempts()); - String lifespan = job.getParameters().getLifespan() == Job.Parameters.IMMORTAL ? "Immortal" - : String.valueOf(job.getParameters().getLifespan()) + " ms"; - return String.format("[%s][%s]%s %s (Time Since Submission: %d ms, Lifespan: %s, Run Attempt: %d/%s)", - id, job.getClass().getSimpleName(), tag, event, timeSinceSubmission, lifespan, runAttempt, maxAttempts); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java deleted file mode 100644 index 5906afd29..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ /dev/null @@ -1,310 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import android.app.Application; -import android.content.Intent; -import android.os.Build; - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.utilities.Data; -import org.session.libsession.utilities.Debouncer; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory; -import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; -import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; - -/** - * Allows the scheduling of durable jobs that will be run as early as possible. - */ -public class JobManager implements ConstraintObserver.Notifier { - - private static final String TAG = JobManager.class.getSimpleName(); - - private final ExecutorService executor; - private final JobController jobController; - private final JobRunner[] jobRunners; - - private final Set emptyQueueListeners = new CopyOnWriteArraySet<>(); - - public JobManager(@NonNull Application application, @NonNull Configuration configuration) { - this.executor = configuration.getExecutorFactory().newSingleThreadExecutor("JobManager"); - this.jobRunners = new JobRunner[configuration.getJobThreadCount()]; - this.jobController = new JobController(application, - configuration.getJobStorage(), - configuration.getJobInstantiator(), - configuration.getConstraintFactories(), - configuration.getDataSerializer(), - Build.VERSION.SDK_INT < 26 ? new AlarmManagerScheduler(application) - : new CompositeScheduler(new InAppScheduler(this), new JobSchedulerScheduler(application)), - new Debouncer(500), - this::onEmptyQueue); - - executor.execute(() -> { - jobController.init(); - - for (int i = 0; i < jobRunners.length; i++) { - jobRunners[i] = new JobRunner(application, i + 1, jobController); - jobRunners[i].start(); - } - - for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) { - constraintObserver.register(this); - } - - if (Build.VERSION.SDK_INT < 26) { - application.startService(new Intent(application, KeepAliveService.class)); - } - - wakeUp(); - }); - } - - /** - * Enqueues a single job to be run. - */ - public void add(@NonNull Job job) { - new Chain(this, Collections.singletonList(job)).enqueue(); - } - - /** - * Begins the creation of a job chain with a single job. - * @see Chain - */ - public Chain startChain(@NonNull Job job) { - return new Chain(this, Collections.singletonList(job)); - } - - /** - * Begins the creation of a job chain with a set of jobs that can be run in parallel. - * @see Chain - */ - public Chain startChain(@NonNull List jobs) { - return new Chain(this, jobs); - } - - /** - * Retrieves a string representing the state of the job queue. Intended for debugging. - */ - public @NonNull String getDebugInfo() { - Future result = executor.submit(jobController::getDebugInfo); - try { - return result.get(); - } catch (ExecutionException | InterruptedException e) { - Log.w(TAG, "Failed to retrieve Job info.", e); - return "Failed to retrieve Job info."; - } - } - - /** - * Adds a listener to that will be notified when the job queue has been drained. - */ - void addOnEmptyQueueListener(@NonNull EmptyQueueListener listener) { - executor.execute(() -> { - emptyQueueListeners.add(listener); - }); - } - - /** - * Removes a listener that was added via {@link #addOnEmptyQueueListener(EmptyQueueListener)}. - */ - void removeOnEmptyQueueListener(@NonNull EmptyQueueListener listener) { - executor.execute(() -> { - emptyQueueListeners.remove(listener); - }); - } - - @Override - public void onConstraintMet(@NonNull String reason) { - Log.i(TAG, "onConstraintMet(" + reason + ")"); - wakeUp(); - } - - /** - * Pokes the system to take another pass at the job queue. - */ - void wakeUp() { - executor.execute(jobController::wakeUp); - } - - private void enqueueChain(@NonNull Chain chain) { - executor.execute(() -> { - jobController.submitNewJobChain(chain.getJobListChain()); - wakeUp(); - }); - } - - private void onEmptyQueue() { - executor.execute(() -> { - for (EmptyQueueListener listener : emptyQueueListeners) { - listener.onQueueEmpty(); - } - }); - } - - public interface EmptyQueueListener { - void onQueueEmpty(); - } - - /** - * Allows enqueuing work that depends on each other. Jobs that appear later in the chain will - * only run after all jobs earlier in the chain have been completed. If a job fails, all jobs - * that occur later in the chain will also be failed. - */ - public static class Chain { - - private final JobManager jobManager; - private final List> jobs; - - private Chain(@NonNull JobManager jobManager, @NonNull List jobs) { - this.jobManager = jobManager; - this.jobs = new LinkedList<>(); - - this.jobs.add(new ArrayList<>(jobs)); - } - - public Chain then(@NonNull Job job) { - return then(Collections.singletonList(job)); - } - - public Chain then(@NonNull List jobs) { - if (!jobs.isEmpty()) { - this.jobs.add(new ArrayList<>(jobs)); - } - return this; - } - - public void enqueue() { - jobManager.enqueueChain(this); - } - - private List> getJobListChain() { - return jobs; - } - } - - public static class Configuration { - - private final ExecutorFactory executorFactory; - private final int jobThreadCount; - private final JobInstantiator jobInstantiator; - private final ConstraintInstantiator constraintInstantiator; - private final List constraintObservers; - private final Data.Serializer dataSerializer; - private final JobStorage jobStorage; - - private Configuration(int jobThreadCount, - @NonNull ExecutorFactory executorFactory, - @NonNull JobInstantiator jobInstantiator, - @NonNull ConstraintInstantiator constraintInstantiator, - @NonNull List constraintObservers, - @NonNull Data.Serializer dataSerializer, - @NonNull JobStorage jobStorage) - { - this.executorFactory = executorFactory; - this.jobThreadCount = jobThreadCount; - this.jobInstantiator = jobInstantiator; - this.constraintInstantiator = constraintInstantiator; - this.constraintObservers = constraintObservers; - this.dataSerializer = dataSerializer; - this.jobStorage = jobStorage; - } - - int getJobThreadCount() { - return jobThreadCount; - } - - @NonNull ExecutorFactory getExecutorFactory() { - return executorFactory; - } - - @NonNull JobInstantiator getJobInstantiator() { - return jobInstantiator; - } - - @NonNull - ConstraintInstantiator getConstraintFactories() { - return constraintInstantiator; - } - - @NonNull List getConstraintObservers() { - return constraintObservers; - } - - @NonNull Data.Serializer getDataSerializer() { - return dataSerializer; - } - - @NonNull JobStorage getJobStorage() { - return jobStorage; - } - - - public static class Builder { - - private ExecutorFactory executorFactory = new DefaultExecutorFactory(); - private int jobThreadCount = 1; - private Map jobFactories = new HashMap<>(); - private Map constraintFactories = new HashMap<>(); - private List constraintObservers = new ArrayList<>(); - private Data.Serializer dataSerializer = new JsonDataSerializer(); - private JobStorage jobStorage = null; - - public @NonNull Builder setJobThreadCount(int jobThreadCount) { - this.jobThreadCount = jobThreadCount; - return this; - } - - public @NonNull Builder setExecutorFactory(@NonNull ExecutorFactory executorFactory) { - this.executorFactory = executorFactory; - return this; - } - - public @NonNull Builder setJobFactories(@NonNull Map jobFactories) { - this.jobFactories = jobFactories; - return this; - } - - public @NonNull Builder setConstraintFactories(@NonNull Map constraintFactories) { - this.constraintFactories = constraintFactories; - return this; - } - - public @NonNull Builder setConstraintObservers(@NonNull List constraintObservers) { - this.constraintObservers = constraintObservers; - return this; - } - - public @NonNull Builder setDataSerializer(@NonNull Data.Serializer dataSerializer) { - this.dataSerializer = dataSerializer; - return this; - } - - public @NonNull Builder setJobStorage(@NonNull JobStorage jobStorage) { - this.jobStorage = jobStorage; - return this; - } - - public @NonNull Configuration build() { - return new Configuration(jobThreadCount, - executorFactory, - new JobInstantiator(jobFactories), - new ConstraintInstantiator(constraintFactories), - new ArrayList<>(constraintObservers), - dataSerializer, - jobStorage); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobRunner.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobRunner.java deleted file mode 100644 index 6eadf0fd5..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobRunner.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import android.app.Application; -import android.os.PowerManager; -import androidx.annotation.NonNull; - -import com.annimon.stream.Stream; - -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.util.WakeLockUtil; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -class JobRunner extends Thread { - - private static final String TAG = JobRunner.class.getSimpleName(); - - private static long WAKE_LOCK_TIMEOUT = TimeUnit.MINUTES.toMillis(10); - - private final Application application; - private final int id; - private final JobController jobController; - - JobRunner(@NonNull Application application, int id, @NonNull JobController jobController) { - super("JobRunner-" + id); - - this.application = application; - this.id = id; - this.jobController = jobController; - } - - @Override - public synchronized void run() { - while (true) { - Job job = jobController.pullNextEligibleJobForExecution(); - Job.Result result = run(job); - - jobController.onJobFinished(job); - - switch (result) { - case SUCCESS: - jobController.onSuccess(job); - break; - case RETRY: - jobController.onRetry(job); - job.onRetry(); - break; - case FAILURE: - List dependents = jobController.onFailure(job); - job.onCanceled(); - Stream.of(dependents).forEach(Job::onCanceled); - break; - } - } - } - - private Job.Result run(@NonNull Job job) { - Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Running job.")); - - if (isJobExpired(job)) { - Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its lifespan.")); - return Job.Result.FAILURE; - } - - Job.Result result = null; - PowerManager.WakeLock wakeLock = null; - - try { - wakeLock = WakeLockUtil.acquire(application, PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TIMEOUT, job.getId()); - result = job.run(); - } catch (Exception e) { - Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing due to an unexpected exception."), e); - return Job.Result.FAILURE; - } finally { - if (wakeLock != null) { - WakeLockUtil.release(wakeLock, job.getId()); - } - } - - printResult(job, result); - - if (result == Job.Result.RETRY && job.getRunAttempt() + 1 >= job.getParameters().getMaxAttempts() && - job.getParameters().getMaxAttempts() != Job.Parameters.UNLIMITED) - { - Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its max number of attempts.")); - return Job.Result.FAILURE; - } - - return result; - } - - private boolean isJobExpired(@NonNull Job job) { - long expirationTime = job.getParameters().getCreateTime() + job.getParameters().getLifespan(); - - if (expirationTime < 0) { - expirationTime = Long.MAX_VALUE; - } - - return job.getParameters().getLifespan() != Job.Parameters.IMMORTAL && expirationTime <= System.currentTimeMillis(); - } - - private void printResult(@NonNull Job job, @NonNull Job.Result result) { - if (result == Job.Result.FAILURE) { - Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Job failed.")); - } else { - Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Job finished with result: " + result)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java deleted file mode 100644 index 40acbf520..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import android.app.Application; -import android.app.job.JobInfo; -import android.app.job.JobParameters; -import android.app.job.JobScheduler; -import android.app.job.JobService; -import android.content.ComponentName; -import android.content.Context; -import android.content.SharedPreferences; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.session.libsignal.utilities.Log; - -import java.util.List; - -@RequiresApi(26) -public class JobSchedulerScheduler implements Scheduler { - - private static final String TAG = JobSchedulerScheduler.class.getSimpleName(); - - private static final String PREF_NAME = "JobSchedulerScheduler_prefs"; - private static final String PREF_NEXT_ID = "pref_next_id"; - - private static final int MAX_ID = 75; - - private final Application application; - - JobSchedulerScheduler(@NonNull Application application) { - this.application = application; - } - - @RequiresApi(26) - @Override - public void schedule(long delay, @NonNull List constraints) { - JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(getNextId(), new ComponentName(application, SystemService.class)) - .setMinimumLatency(delay) - .setPersisted(true); - - for (Constraint constraint : constraints) { - constraint.applyToJobInfo(jobInfoBuilder); - } - - Log.i(TAG, "Scheduling a run in " + delay + " ms."); - JobScheduler jobScheduler = application.getSystemService(JobScheduler.class); - jobScheduler.schedule(jobInfoBuilder.build()); - } - - private int getNextId() { - SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - int returnedId = prefs.getInt(PREF_NEXT_ID, 0); - int nextId = returnedId + 1 > MAX_ID ? 0 : returnedId + 1; - - prefs.edit().putInt(PREF_NEXT_ID, nextId).apply(); - - return returnedId; - } - - @RequiresApi(api = 26) - public static class SystemService extends JobService { - - @Override - public boolean onStartJob(JobParameters params) { - Log.d(TAG, "onStartJob()"); - - JobManager jobManager = ApplicationContext.getInstance(getApplicationContext()).getJobManager(); - - jobManager.addOnEmptyQueueListener(new JobManager.EmptyQueueListener() { - @Override - public void onQueueEmpty() { - jobManager.removeOnEmptyQueueListener(this); - jobFinished(params, false); - Log.d(TAG, "jobFinished()"); - } - }); - - jobManager.wakeUp(); - - return true; - } - - @Override - public boolean onStopJob(JobParameters params) { - Log.d(TAG, "onStopJob()"); - return false; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Scheduler.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Scheduler.java deleted file mode 100644 index 194acd39b..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Scheduler.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import androidx.annotation.NonNull; - -import java.util.List; - -public interface Scheduler { - void schedule(long delay, @NonNull List constraints); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraint.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraint.java deleted file mode 100644 index 6d6fc0499..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraint.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.impl; - -import android.app.Application; -import android.app.job.JobInfo; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.Constraint; -import org.thoughtcrime.securesms.sms.TelephonyServiceState; - -public class CellServiceConstraint implements Constraint { - - public static final String KEY = "CellServiceConstraint"; - - private final Application application; - - public CellServiceConstraint(@NonNull Application application) { - this.application = application; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public boolean isMet() { - TelephonyServiceState telephonyServiceState = new TelephonyServiceState(); - return telephonyServiceState.isConnected(application); - } - - @Override - public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) { - } - - public static final class Factory implements Constraint.Factory { - - private final Application application; - - public Factory(@NonNull Application application) { - this.application = application; - } - - @Override - public CellServiceConstraint create() { - return new CellServiceConstraint(application); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraintObserver.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraintObserver.java deleted file mode 100644 index fd0971dc5..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraintObserver.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.impl; - -import android.app.Application; -import android.content.Context; -import androidx.annotation.NonNull; -import android.telephony.PhoneStateListener; -import android.telephony.ServiceState; -import android.telephony.TelephonyManager; - -import org.thoughtcrime.securesms.jobmanager.ConstraintObserver; - -public class CellServiceConstraintObserver implements ConstraintObserver { - - private static final String REASON = CellServiceConstraintObserver.class.getSimpleName(); - - private Notifier notifier; - - public CellServiceConstraintObserver(@NonNull Application application) { - TelephonyManager telephonyManager = (TelephonyManager) application.getSystemService(Context.TELEPHONY_SERVICE); - ServiceStateListener serviceStateListener = new ServiceStateListener(); - - telephonyManager.listen(serviceStateListener, PhoneStateListener.LISTEN_SERVICE_STATE); - } - - @Override - public void register(@NonNull Notifier notifier) { - this.notifier = notifier; - } - - private class ServiceStateListener extends PhoneStateListener { - @Override - public void onServiceStateChanged(ServiceState serviceState) { - if (serviceState.getState() == ServiceState.STATE_IN_SERVICE && notifier != null) { - notifier.onConstraintMet(REASON); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/DefaultExecutorFactory.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/DefaultExecutorFactory.java deleted file mode 100644 index a9d459100..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/DefaultExecutorFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.impl; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.ExecutorFactory; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class DefaultExecutorFactory implements ExecutorFactory { - @Override - public @NonNull ExecutorService newSingleThreadExecutor(@NonNull String name) { - return Executors.newSingleThreadExecutor(r -> new Thread(r, name)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraintObserver.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraintObserver.java deleted file mode 100644 index ef4a61c7c..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraintObserver.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.impl; - -import android.app.Application; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.ConnectivityManager; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.ConstraintObserver; - -public class NetworkConstraintObserver implements ConstraintObserver { - - private static final String REASON = NetworkConstraintObserver.class.getSimpleName(); - - private final Application application; - - public NetworkConstraintObserver(Application application) { - this.application = application; - } - - @Override - public void register(@NonNull Notifier notifier) { - application.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - NetworkConstraint constraint = new NetworkConstraint.Factory(application).create(); - - if (constraint.isMet()) { - notifier.onConstraintMet(REASON); - } - } - }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/NetworkOrCellServiceConstraint.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/NetworkOrCellServiceConstraint.java deleted file mode 100644 index c17931f97..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/NetworkOrCellServiceConstraint.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.impl; - -import android.app.Application; -import android.app.job.JobInfo; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.Constraint; - -public class NetworkOrCellServiceConstraint implements Constraint { - - public static final String KEY = "NetworkOrCellServiceConstraint"; - - private final NetworkConstraint networkConstraint; - private final CellServiceConstraint serviceConstraint; - - public NetworkOrCellServiceConstraint(@NonNull Application application) { - networkConstraint = new NetworkConstraint.Factory(application).create(); - serviceConstraint = new CellServiceConstraint.Factory(application).create(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public boolean isMet() { - return networkConstraint.isMet() || serviceConstraint.isMet(); - } - - @Override - public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) { - } - - public static class Factory implements Constraint.Factory { - - private final Application application; - - public Factory(@NonNull Application application) { - this.application = application; - } - - @Override - public NetworkOrCellServiceConstraint create() { - return new NetworkOrCellServiceConstraint(application); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraint.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraint.java deleted file mode 100644 index 32fa84b6f..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraint.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.impl; - -import android.app.Application; -import android.app.job.JobInfo; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.Constraint; -import org.session.libsession.utilities.TextSecurePreferences; - -public class SqlCipherMigrationConstraint implements Constraint { - - public static final String KEY = "SqlCipherMigrationConstraint"; - - private final Application application; - - private SqlCipherMigrationConstraint(@NonNull Application application) { - this.application = application; - } - - @Override - public boolean isMet() { - return !TextSecurePreferences.getNeedsSqlCipherMigration(application); - } - - @NonNull - @Override - public String getFactoryKey() { - return KEY; - } - - @Override - public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) { - } - - public static final class Factory implements Constraint.Factory { - - private final Application application; - - public Factory(@NonNull Application application) { - this.application = application; - } - - @Override - public SqlCipherMigrationConstraint create() { - return new SqlCipherMigrationConstraint(application); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraintObserver.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraintObserver.java deleted file mode 100644 index 0c9225434..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraintObserver.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.impl; - -import androidx.annotation.NonNull; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.thoughtcrime.securesms.jobmanager.ConstraintObserver; - -public class SqlCipherMigrationConstraintObserver implements ConstraintObserver { - - private static final String REASON = SqlCipherMigrationConstraintObserver.class.getSimpleName(); - - private Notifier notifier; - - public SqlCipherMigrationConstraintObserver() { - EventBus.getDefault().register(this); - } - - @Override - public void register(@NonNull Notifier notifier) { - this.notifier = notifier; - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEvent(SqlCipherNeedsMigrationEvent event) { - if (notifier != null) notifier.onConstraintMet(REASON); - } - - public static class SqlCipherNeedsMigrationEvent { - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/ConstraintSpec.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/ConstraintSpec.java deleted file mode 100644 index 1dab10ae5..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/ConstraintSpec.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.persistence; - -import androidx.annotation.NonNull; - -import java.util.Objects; - -public final class ConstraintSpec { - - private final String jobSpecId; - private final String factoryKey; - - public ConstraintSpec(@NonNull String jobSpecId, @NonNull String factoryKey) { - this.jobSpecId = jobSpecId; - this.factoryKey = factoryKey; - } - - public String getJobSpecId() { - return jobSpecId; - } - - public String getFactoryKey() { - return factoryKey; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ConstraintSpec that = (ConstraintSpec) o; - return Objects.equals(jobSpecId, that.jobSpecId) && - Objects.equals(factoryKey, that.factoryKey); - } - - @Override - public int hashCode() { - return Objects.hash(jobSpecId, factoryKey); - } - - @Override - public @NonNull String toString() { - return String.format("jobSpecId: %s | factoryKey: %s", jobSpecId, factoryKey); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/DependencySpec.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/DependencySpec.java deleted file mode 100644 index 2faea0485..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/DependencySpec.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.persistence; - -import androidx.annotation.NonNull; - -import java.util.Objects; - -public final class DependencySpec { - - private final String jobId; - private final String dependsOnJobId; - - public DependencySpec(@NonNull String jobId, @NonNull String dependsOnJobId) { - this.jobId = jobId; - this.dependsOnJobId = dependsOnJobId; - } - - public @NonNull String getJobId() { - return jobId; - } - - public @NonNull String getDependsOnJobId() { - return dependsOnJobId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DependencySpec that = (DependencySpec) o; - return Objects.equals(jobId, that.jobId) && - Objects.equals(dependsOnJobId, that.dependsOnJobId); - } - - @Override - public int hashCode() { - return Objects.hash(jobId, dependsOnJobId); - } - - @Override - public @NonNull String toString() { - return String.format("jobSpecId: %s | dependsOnJobSpecId: %s", jobId, dependsOnJobId); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java deleted file mode 100644 index f93c0e64b..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.persistence; - -import androidx.annotation.NonNull; - -import java.util.List; -import java.util.Objects; - -public final class FullSpec { - - private final JobSpec jobSpec; - private final List constraintSpecs; - private final List dependencySpecs; - - public FullSpec(@NonNull JobSpec jobSpec, - @NonNull List constraintSpecs, - @NonNull List dependencySpecs) - { - this.jobSpec = jobSpec; - this.constraintSpecs = constraintSpecs; - this.dependencySpecs = dependencySpecs; - } - - public @NonNull JobSpec getJobSpec() { - return jobSpec; - } - - public @NonNull List getConstraintSpecs() { - return constraintSpecs; - } - - public @NonNull List getDependencySpecs() { - return dependencySpecs; - } - - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - FullSpec fullSpec = (FullSpec) o; - return Objects.equals(jobSpec, fullSpec.jobSpec) && - Objects.equals(constraintSpecs, fullSpec.constraintSpecs) && - Objects.equals(dependencySpecs, fullSpec.dependencySpecs); - } - - @Override - public int hashCode() { - return Objects.hash(jobSpec, constraintSpecs, dependencySpecs); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/JobSpec.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/JobSpec.java deleted file mode 100644 index d5f5cd5b3..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/JobSpec.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.persistence; - -import android.annotation.SuppressLint; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Objects; - -public final class JobSpec { - - private final String id; - private final String factoryKey; - private final String queueKey; - private final long createTime; - private final long nextRunAttemptTime; - private final int runAttempt; - private final int maxAttempts; - private final long maxBackoff; - private final long lifespan; - private final int maxInstances; - private final String serializedData; - private final boolean isRunning; - - public JobSpec(@NonNull String id, - @NonNull String factoryKey, - @Nullable String queueKey, - long createTime, - long nextRunAttemptTime, - int runAttempt, - int maxAttempts, - long maxBackoff, - long lifespan, - int maxInstances, - @NonNull String serializedData, - boolean isRunning) - { - this.id = id; - this.factoryKey = factoryKey; - this.queueKey = queueKey; - this.createTime = createTime; - this.nextRunAttemptTime = nextRunAttemptTime; - this.maxBackoff = maxBackoff; - this.runAttempt = runAttempt; - this.maxAttempts = maxAttempts; - this.lifespan = lifespan; - this.maxInstances = maxInstances; - this.serializedData = serializedData; - this.isRunning = isRunning; - } - - public @NonNull String getId() { - return id; - } - - public @NonNull String getFactoryKey() { - return factoryKey; - } - - public @Nullable String getQueueKey() { - return queueKey; - } - - public long getCreateTime() { - return createTime; - } - - public long getNextRunAttemptTime() { - return nextRunAttemptTime; - } - - public int getRunAttempt() { - return runAttempt; - } - - public int getMaxAttempts() { - return maxAttempts; - } - - public long getMaxBackoff() { - return maxBackoff; - } - - public int getMaxInstances() { - return maxInstances; - } - - public long getLifespan() { - return lifespan; - } - - public @NonNull String getSerializedData() { - return serializedData; - } - - public boolean isRunning() { - return isRunning; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - JobSpec jobSpec = (JobSpec) o; - return createTime == jobSpec.createTime && - nextRunAttemptTime == jobSpec.nextRunAttemptTime && - runAttempt == jobSpec.runAttempt && - maxAttempts == jobSpec.maxAttempts && - maxBackoff == jobSpec.maxBackoff && - lifespan == jobSpec.lifespan && - maxInstances == jobSpec.maxInstances && - isRunning == jobSpec.isRunning && - Objects.equals(id, jobSpec.id) && - Objects.equals(factoryKey, jobSpec.factoryKey) && - Objects.equals(queueKey, jobSpec.queueKey) && - Objects.equals(serializedData, jobSpec.serializedData); - } - - @Override - public int hashCode() { - return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, maxInstances, serializedData, isRunning); - } - - @SuppressLint("DefaultLocale") - @Override - public @NonNull String toString() { - return String.format("id: %s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | maxBackoff: %d | maxInstances: %d | lifespan: %d | isRunning: %b | data: %s", - id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, maxInstances, lifespan, isRunning, serializedData); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/JobStorage.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/JobStorage.java deleted file mode 100644 index b7c035ac6..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/persistence/JobStorage.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.persistence; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import java.util.List; - -public interface JobStorage { - - @WorkerThread - void init(); - - @WorkerThread - void insertJobs(@NonNull List fullSpecs); - - @WorkerThread - @Nullable JobSpec getJobSpec(@NonNull String id); - - @WorkerThread - @NonNull List getAllJobSpecs(); - - @WorkerThread - @NonNull List getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime); - - @WorkerThread - int getJobInstanceCount(@NonNull String factoryKey); - - @WorkerThread - void updateJobRunningState(@NonNull String id, boolean isRunning); - - @WorkerThread - void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime); - - @WorkerThread - void updateAllJobsToBePending(); - - @WorkerThread - void deleteJob(@NonNull String id); - - @WorkerThread - void deleteJobs(@NonNull List ids); - - @WorkerThread - @NonNull List getConstraintSpecs(@NonNull String jobId); - - @WorkerThread - @NonNull List getAllConstraintSpecs(); - - @WorkerThread - @NonNull List getDependencySpecsThatDependOnJob(@NonNull String jobSpecId); - - @WorkerThread - @NonNull List getAllDependencySpecs(); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java deleted file mode 100644 index 0bf7ea24e..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.graphics.Bitmap; - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.utilities.Data; -import org.session.libsession.utilities.DownloadUtilities; -import org.session.libsession.utilities.GroupRecord; -import org.session.libsignal.exceptions.InvalidMessageException; -import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException; -import org.session.libsignal.messages.SignalServiceAttachmentPointer; -import org.session.libsignal.streams.AttachmentCipherInputStream; -import org.session.libsignal.utilities.Hex; -import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.guava.Optional; -import org.thoughtcrime.securesms.database.GroupDatabase; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; -import org.thoughtcrime.securesms.util.BitmapDecodingException; -import org.thoughtcrime.securesms.util.BitmapUtil; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class AvatarDownloadJob extends BaseJob { - - public static final String KEY = "AvatarDownloadJob"; - - private static final String TAG = AvatarDownloadJob.class.getSimpleName(); - - private static final int MAX_AVATAR_SIZE = 20 * 1024 * 1024; - - private static final String KEY_GROUP_ID = "group_id"; - - private String groupId; - - public AvatarDownloadJob(@NonNull String groupId) { - this(new Job.Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setMaxAttempts(10) - .build(), - groupId); - } - - private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull String groupId) { - super(parameters); - this.groupId = groupId; - } - - @Override - public @NonNull Data serialize() { - return new Data.Builder().putString(KEY_GROUP_ID, groupId).build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws IOException { - GroupDatabase database = DatabaseComponent.get(context).groupDatabase(); - Optional record = database.getGroup(groupId); - File attachment = null; - - try { - if (record.isPresent()) { - long avatarId = record.get().getAvatarId(); - String contentType = record.get().getAvatarContentType(); - byte[] key = record.get().getAvatarKey(); - String relay = record.get().getRelay(); - Optional digest = Optional.fromNullable(record.get().getAvatarDigest()); - Optional fileName = Optional.absent(); - String url = record.get().getUrl(); - - if (avatarId == -1 || key == null || url.isEmpty()) { - return; - } - - if (digest.isPresent()) { - Log.i(TAG, "Downloading group avatar with digest: " + Hex.toString(digest.get())); - } - - attachment = File.createTempFile("avatar", "tmp", context.getCacheDir()); - attachment.deleteOnExit(); - - SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url); - - if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL."); - DownloadUtilities.downloadFile(attachment, pointer.getUrl()); - - // Assume we're retrieving an attachment for an open group server if the digest is not set - InputStream inputStream; - if (!pointer.getDigest().isPresent()) { - inputStream = new FileInputStream(attachment); - } else { - inputStream = AttachmentCipherInputStream.createForAttachment(attachment, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get()); - } - - Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); - - database.updateProfilePicture(groupId, avatar); - inputStream.close(); - } - } catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) { - Log.w(TAG, e); - } finally { - if (attachment != null) - attachment.delete(); - } - } - - @Override - public void onCanceled() {} - - @Override - public boolean onShouldRetry(@NonNull Exception exception) { - if (exception instanceof IOException) return true; - return false; - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new AvatarDownloadJob(parameters, data.getString(KEY_GROUP_ID)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BaseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/BaseJob.java deleted file mode 100644 index 0c11cc552..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BaseJob.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobLogger; -import org.session.libsignal.utilities.Log; - -/** - * @deprecated - * use WorkManager - * API instead. - */ -public abstract class BaseJob extends Job { - - private static final String TAG = BaseJob.class.getSimpleName(); - - public BaseJob(@NonNull Parameters parameters) { - super(parameters); - } - - @Override - public @NonNull Result run() { - try { - onRun(); - return Result.SUCCESS; - } catch (Exception e) { - if (onShouldRetry(e)) { - Log.i(TAG, JobLogger.format(this, "Encountered a retryable exception."), e); - return Result.RETRY; - } else { - Log.w(TAG, JobLogger.format(this, "Encountered a failing exception."), e); - return Result.FAILURE; - } - } - } - - protected abstract void onRun() throws Exception; - - protected abstract boolean onShouldRetry(@NonNull Exception e); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/FastJobStorage.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/FastJobStorage.java deleted file mode 100644 index 3b50dc273..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/FastJobStorage.java +++ /dev/null @@ -1,261 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.annimon.stream.Stream; - -import org.thoughtcrime.securesms.database.JobDatabase; -import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec; -import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; - -import org.session.libsession.utilities.Util; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Set; - -public class FastJobStorage implements JobStorage { - - private final JobDatabase jobDatabase; - - private final List jobs; - private final Map> constraintsByJobId; - private final Map> dependenciesByJobId; - - public FastJobStorage(@NonNull JobDatabase jobDatabase) { - this.jobDatabase = jobDatabase; - this.jobs = new ArrayList<>(); - this.constraintsByJobId = new HashMap<>(); - this.dependenciesByJobId = new HashMap<>(); - } - - @Override - public synchronized void init() { - List jobSpecs = jobDatabase.getAllJobSpecs(); - List constraintSpecs = jobDatabase.getAllConstraintSpecs(); - List dependencySpecs = jobDatabase.getAllDependencySpecs(); - - jobs.addAll(jobSpecs); - - for (ConstraintSpec constraintSpec: constraintSpecs) { - List jobConstraints = Util.getOrDefault(constraintsByJobId, constraintSpec.getJobSpecId(), new LinkedList<>()); - jobConstraints.add(constraintSpec); - constraintsByJobId.put(constraintSpec.getJobSpecId(), jobConstraints); - } - - for (DependencySpec dependencySpec : dependencySpecs) { - List jobDependencies = Util.getOrDefault(dependenciesByJobId, dependencySpec.getJobId(), new LinkedList<>()); - jobDependencies.add(dependencySpec); - dependenciesByJobId.put(dependencySpec.getJobId(), jobDependencies); - } - } - - @Override - public synchronized void insertJobs(@NonNull List fullSpecs) { - jobDatabase.insertJobs(fullSpecs); - - for (FullSpec fullSpec : fullSpecs) { - jobs.add(fullSpec.getJobSpec()); - constraintsByJobId.put(fullSpec.getJobSpec().getId(), fullSpec.getConstraintSpecs()); - dependenciesByJobId.put(fullSpec.getJobSpec().getId(), fullSpec.getDependencySpecs()); - } - } - - @Override - public synchronized @Nullable JobSpec getJobSpec(@NonNull String id) { - for (JobSpec jobSpec : jobs) { - if (jobSpec.getId().equals(id)) { - return jobSpec; - } - } - return null; - } - - @Override - public synchronized @NonNull List getAllJobSpecs() { - return new ArrayList<>(jobs); - } - - @Override - public synchronized @NonNull List getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime) { - return Stream.of(jobs) - .filter(j -> JobManagerFactories.hasFactoryForKey(j.getFactoryKey())) - .filterNot(JobSpec::isRunning) - .filter(this::firstInQueue) - .filter(j -> !dependenciesByJobId.containsKey(j.getId()) || dependenciesByJobId.get(j.getId()).isEmpty()) - .filter(j -> j.getNextRunAttemptTime() <= currentTime) - .sorted((j1, j2) -> Long.compare(j1.getCreateTime(), j2.getCreateTime())) - .toList(); - } - - private boolean firstInQueue(@NonNull JobSpec job) { - if (job.getQueueKey() == null) { - return true; - } - - return Stream.of(jobs) - .filter(j -> Util.equals(j.getQueueKey(), job.getQueueKey())) - .sorted((j1, j2) -> Long.compare(j1.getCreateTime(), j2.getCreateTime())) - .toList() - .get(0) - .equals(job); - } - - @Override - public synchronized int getJobInstanceCount(@NonNull String factoryKey) { - return (int) Stream.of(jobs) - .filter(j -> j.getFactoryKey().equals(factoryKey)) - .count(); - } - - @Override - public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) { - jobDatabase.updateJobRunningState(id, isRunning); - - ListIterator iter = jobs.listIterator(); - - while (iter.hasNext()) { - JobSpec existing = iter.next(); - if (existing.getId().equals(id)) { - JobSpec updated = new JobSpec(existing.getId(), - existing.getFactoryKey(), - existing.getQueueKey(), - existing.getCreateTime(), - existing.getNextRunAttemptTime(), - existing.getRunAttempt(), - existing.getMaxAttempts(), - existing.getMaxBackoff(), - existing.getLifespan(), - existing.getMaxInstances(), - existing.getSerializedData(), - isRunning); - iter.set(updated); - } - } - } - - @Override - public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime) { - jobDatabase.updateJobAfterRetry(id, isRunning, runAttempt, nextRunAttemptTime); - - ListIterator iter = jobs.listIterator(); - - while (iter.hasNext()) { - JobSpec existing = iter.next(); - if (existing.getId().equals(id)) { - JobSpec updated = new JobSpec(existing.getId(), - existing.getFactoryKey(), - existing.getQueueKey(), - existing.getCreateTime(), - nextRunAttemptTime, - runAttempt, - existing.getMaxAttempts(), - existing.getMaxBackoff(), - existing.getLifespan(), - existing.getMaxInstances(), - existing.getSerializedData(), - isRunning); - iter.set(updated); - } - } - } - - @Override - public synchronized void updateAllJobsToBePending() { - jobDatabase.updateAllJobsToBePending(); - - ListIterator iter = jobs.listIterator(); - - while (iter.hasNext()) { - JobSpec existing = iter.next(); - JobSpec updated = new JobSpec(existing.getId(), - existing.getFactoryKey(), - existing.getQueueKey(), - existing.getCreateTime(), - existing.getNextRunAttemptTime(), - existing.getRunAttempt(), - existing.getMaxAttempts(), - existing.getMaxBackoff(), - existing.getLifespan(), - existing.getMaxInstances(), - existing.getSerializedData(), - false); - iter.set(updated); - } - } - - @Override - public synchronized void deleteJob(@NonNull String jobId) { - deleteJobs(Collections.singletonList(jobId)); - } - - @Override - public synchronized void deleteJobs(@NonNull List jobIds) { - jobDatabase.deleteJobs(jobIds); - - Set deleteIds = new HashSet<>(jobIds); - - Iterator jobIter = jobs.iterator(); - while (jobIter.hasNext()) { - if (deleteIds.contains(jobIter.next().getId())) { - jobIter.remove(); - } - } - - for (String jobId : jobIds) { - constraintsByJobId.remove(jobId); - dependenciesByJobId.remove(jobId); - - for (Map.Entry> entry : dependenciesByJobId.entrySet()) { - Iterator depedencyIter = entry.getValue().iterator(); - - while (depedencyIter.hasNext()) { - if (depedencyIter.next().getDependsOnJobId().equals(jobId)) { - depedencyIter.remove(); - } - } - } - } - } - - @Override - public synchronized @NonNull List getConstraintSpecs(@NonNull String jobId) { - return Util.getOrDefault(constraintsByJobId, jobId, new LinkedList<>()); - } - - @Override - public synchronized @NonNull List getAllConstraintSpecs() { - return Stream.of(constraintsByJobId) - .map(Map.Entry::getValue) - .flatMap(Stream::of) - .toList(); - } - - @Override - public synchronized @NonNull List getDependencySpecsThatDependOnJob(@NonNull String jobSpecId) { - return Stream.of(dependenciesByJobId.entrySet()) - .map(Map.Entry::getValue) - .flatMap(Stream::of) - .filter(j -> j.getDependsOnJobId().equals(jobSpecId)) - .toList(); - } - - @Override - public @NonNull List getAllDependencySpecs() { - return Stream.of(dependenciesByJobId) - .map(Map.Entry::getValue) - .flatMap(Stream::of) - .toList(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java deleted file mode 100644 index ef73325f3..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.app.Application; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.Constraint; -import org.thoughtcrime.securesms.jobmanager.ConstraintObserver; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraint; -import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; -import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; -import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public final class JobManagerFactories { - - private static Collection factoryKeys = new ArrayList<>(); - - public static Map getJobFactories(@NonNull Application application) { - HashMap factoryHashMap = new HashMap() {{ - put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory()); - put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); - put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application)); - put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); - put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory()); - }}; - factoryKeys.addAll(factoryHashMap.keySet()); - return factoryHashMap; - } - - public static Map getConstraintFactories(@NonNull Application application) { - return new HashMap() {{ - put(CellServiceConstraint.KEY, new CellServiceConstraint.Factory(application)); - put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application)); - put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application)); - put(SqlCipherMigrationConstraint.KEY, new SqlCipherMigrationConstraint.Factory(application)); - }}; - } - - public static List getConstraintObservers(@NonNull Application application) { - return Arrays.asList(new CellServiceConstraintObserver(application), - new NetworkConstraintObserver(application), - new SqlCipherMigrationConstraintObserver()); - } - - public static boolean hasFactoryForKey(String factoryKey) { - return factoryKeys.contains(factoryKey); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java deleted file mode 100644 index e5715db26..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.utilities.Data; -import org.session.libsignal.utilities.NoExternalStorageException; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.database.BackupFileRecord; -import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.service.GenericForegroundService; -import org.thoughtcrime.securesms.util.BackupUtil; - -import java.io.IOException; -import java.util.Collections; - -import network.loki.messenger.R; - -public class LocalBackupJob extends BaseJob { - - public static final String KEY = "LocalBackupJob"; - - private static final String TAG = LocalBackupJob.class.getSimpleName(); - - public LocalBackupJob() { - this(new Job.Parameters.Builder() - .setQueue("__LOCAL_BACKUP__") - .setMaxInstances(1) - .setMaxAttempts(3) - .build()); - } - - private LocalBackupJob(@NonNull Job.Parameters parameters) { - super(parameters); - } - - @Override - public @NonNull - Data serialize() { - return Data.EMPTY; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws NoExternalStorageException, IOException { - Log.i(TAG, "Executing backup job..."); - - GenericForegroundService.startForegroundTask(context, - context.getString(R.string.LocalBackupJob_creating_backup), - NotificationChannels.BACKUPS, - R.drawable.ic_launcher_foreground); - - // TODO: Maybe create a new backup icon like ic_signal_backup? - - try { - BackupFileRecord record = BackupUtil.createBackupFile(context); - BackupUtil.deleteAllBackupFiles(context, Collections.singletonList(record)); - - } finally { - GenericForegroundService.stopForegroundTask(context); - } - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - return false; - } - - @Override - public void onCanceled() { - } - - public static class Factory implements Job.Factory { - @Override - public @NonNull LocalBackupJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new LocalBackupJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PrepareAttachmentAudioExtrasJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PrepareAttachmentAudioExtrasJob.kt deleted file mode 100644 index 69794d41b..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PrepareAttachmentAudioExtrasJob.kt +++ /dev/null @@ -1,133 +0,0 @@ -package org.thoughtcrime.securesms.jobs - -import android.os.Build -import org.greenrobot.eventbus.EventBus -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.DatabaseAttachmentAudioExtras -import org.session.libsession.messaging.utilities.Data -import org.session.libsession.utilities.DecodedAudio -import org.session.libsession.utilities.InputStreamMediaDataSource -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobs.PrepareAttachmentAudioExtrasJob.AudioExtrasUpdatedEvent -import org.thoughtcrime.securesms.mms.PartAuthority -import java.util.* -import java.util.concurrent.TimeUnit - -/** - * Decodes the audio content of the related attachment entry - * and caches the result with [DatabaseAttachmentAudioExtras] data. - * - * It only process attachments with "audio" mime types. - * - * Due to [DecodedAudio] implementation limitations, it only works for API 23+. - * For any lower targets fake data will be generated. - * - * You can subscribe to [AudioExtrasUpdatedEvent] to be notified about the successful result. - */ -//TODO AC: Rewrite to WorkManager API when -// https://github.com/loki-project/session-android/pull/354 is merged. -class PrepareAttachmentAudioExtrasJob : BaseJob { - - companion object { - private const val TAG = "AttachAudioExtrasJob" - - const val KEY = "PrepareAttachmentAudioExtrasJob" - const val DATA_ATTACH_ID = "attachment_id" - - const val VISUAL_RMS_FRAMES = 32 // The amount of values to be computed for the visualization. - } - - private val attachmentId: AttachmentId - - constructor(attachmentId: AttachmentId) : this(Parameters.Builder() - .setQueue(KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .build(), - attachmentId) - - private constructor(parameters: Parameters, attachmentId: AttachmentId) : super(parameters) { - this.attachmentId = attachmentId - } - - override fun serialize(): Data { - return Data.Builder().putParcelable(DATA_ATTACH_ID, attachmentId).build(); - } - - override fun getFactoryKey(): String { return KEY - } - - override fun onShouldRetry(e: Exception): Boolean { - return false - } - - override fun onCanceled() { } - - override fun onRun() { - Log.v(TAG, "Processing attachment: $attachmentId") - - val attachDb = DatabaseComponent.get(context).attachmentDatabase() - val attachment = attachDb.getAttachment(attachmentId) - - if (attachment == null) { - throw IllegalStateException("Cannot find attachment with the ID $attachmentId") - } - if (!attachment.contentType.startsWith("audio/")) { - throw IllegalStateException("Attachment $attachmentId is not of audio type.") - } - - // Check if the audio extras already exist. - if (attachDb.getAttachmentAudioExtras(attachmentId) != null) return - - fun extractAttachmentRandomSeed(attachment: Attachment): Int { - return when { - attachment.digest != null -> attachment.digest!!.sum() - attachment.fileName != null -> attachment.fileName.hashCode() - else -> attachment.hashCode() - } - } - - fun generateFakeRms(seed: Int, frames: Int = VISUAL_RMS_FRAMES): ByteArray { - return ByteArray(frames).apply { Random(seed.toLong()).nextBytes(this) } - } - - var rmsValues: ByteArray - var totalDurationMs: Long = DatabaseAttachmentAudioExtras.DURATION_UNDEFINED - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - // Due to API version incompatibility, we just display some random waveform for older API. - rmsValues = generateFakeRms(extractAttachmentRandomSeed(attachment)) - } else { - try { - @Suppress("BlockingMethodInNonBlockingContext") - val decodedAudio = PartAuthority.getAttachmentStream(context, attachment.dataUri!!).use { - DecodedAudio.create(InputStreamMediaDataSource(it)) - } - rmsValues = decodedAudio.calculateRms(VISUAL_RMS_FRAMES) - totalDurationMs = (decodedAudio.totalDuration / 1000.0).toLong() - } catch (e: Exception) { - Log.w(TAG, "Failed to decode sample values for the audio attachment \"${attachment.fileName}\".", e) - rmsValues = generateFakeRms(extractAttachmentRandomSeed(attachment)) - } - } - - attachDb.setAttachmentAudioExtras(DatabaseAttachmentAudioExtras( - attachmentId, - rmsValues, - totalDurationMs - )) - - EventBus.getDefault().post(AudioExtrasUpdatedEvent(attachmentId)) - } - - class Factory : Job.Factory { - override fun create(parameters: Parameters, data: Data): PrepareAttachmentAudioExtrasJob { - return PrepareAttachmentAudioExtrasJob(parameters, data.getParcelable(DATA_ATTACH_ID, AttachmentId.CREATOR)) - } - } - - /** Gets dispatched once the audio extras have been updated. */ - data class AudioExtrasUpdatedEvent(val attachmentId: AttachmentId) -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java deleted file mode 100644 index 39b775303..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.app.Application; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import org.session.libsession.avatars.AvatarHelper; -import org.session.libsession.messaging.utilities.Data; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.DownloadUtilities; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsignal.exceptions.PushNetworkException; -import org.session.libsignal.streams.ProfileCipherInputStream; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.SecureRandom; -import java.util.concurrent.TimeUnit; - -public class RetrieveProfileAvatarJob extends BaseJob { - - public static final String KEY = "RetrieveProfileAvatarJob"; - - private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName(); - - private static final int MAX_PROFILE_SIZE_BYTES = 10 * 1024 * 1024; - - private static final String KEY_PROFILE_AVATAR = "profile_avatar"; - private static final String KEY_ADDRESS = "address"; - - - private String profileAvatar; - private Recipient recipient; - - public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) { - this(new Job.Parameters.Builder() - .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.HOURS.toMillis(1)) - .setMaxAttempts(2) - .setMaxInstances(1) - .build(), - recipient, - profileAvatar); - } - - private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) { - super(parameters); - this.recipient = recipient; - this.profileAvatar = profileAvatar; - } - - @Override - public @NonNull - Data serialize() { - return new Data.Builder() - .putString(KEY_PROFILE_AVATAR, profileAvatar) - .putString(KEY_ADDRESS, recipient.getAddress().serialize()) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws IOException { - RecipientDatabase database = DatabaseComponent.get(context).recipientDatabase(); - byte[] profileKey = recipient.resolve().getProfileKey(); - - if (profileKey == null || (profileKey.length != 32 && profileKey.length != 16)) { - Log.w(TAG, "Recipient profile key is gone!"); - return; - } - - if (AvatarHelper.avatarFileExists(context, recipient.resolve().getAddress()) && Util.equals(profileAvatar, recipient.resolve().getProfileAvatar())) { - Log.w(TAG, "Already retrieved profile avatar: " + profileAvatar); - return; - } - - if (TextUtils.isEmpty(profileAvatar)) { - Log.w(TAG, "Removing profile avatar for: " + recipient.getAddress().serialize()); - AvatarHelper.delete(context, recipient.getAddress()); - database.setProfileAvatar(recipient, profileAvatar); - return; - } - - File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir()); - - try { - DownloadUtilities.downloadFile(downloadDestination, profileAvatar); - InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey); - File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir()); - - Util.copy(avatarStream, new FileOutputStream(decryptDestination)); - decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress())); - } finally { - if (downloadDestination != null) downloadDestination.delete(); - } - - if (recipient.isLocalNumber()) { - TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt()); - } - database.setProfileAvatar(recipient, profileAvatar); - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - if (e instanceof PushNetworkException) return true; - return false; - } - - @Override - public void onCanceled() { - } - - public static final class Factory implements Job.Factory { - - private final Application application; - - public Factory(Application application) { - this.application = application; - } - - @Override - public @NonNull RetrieveProfileAvatarJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new RetrieveProfileAvatarJob(parameters, - Recipient.from(application, Address.fromSerialized(data.getString(KEY_ADDRESS)), true), - data.getString(KEY_PROFILE_AVATAR)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/UpdateApkJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/UpdateApkJob.java deleted file mode 100644 index 5b4ce8d13..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/UpdateApkJob.java +++ /dev/null @@ -1,271 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - - -import android.app.DownloadManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import org.session.libsession.messaging.utilities.Data; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.service.UpdateApkReadyListener; -import org.session.libsession.utilities.FileUtils; -import org.session.libsignal.utilities.Hex; -import org.session.libsignal.utilities.JsonUtil; -import org.session.libsession.utilities.TextSecurePreferences; - -import java.io.FileInputStream; -import java.io.IOException; -import java.security.MessageDigest; - -import network.loki.messenger.BuildConfig; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -public class UpdateApkJob extends BaseJob { - - public static final String KEY = "UpdateApkJob"; - - private static final String TAG = UpdateApkJob.class.getSimpleName(); - - public UpdateApkJob() { - this(new Job.Parameters.Builder() - .setQueue("UpdateApkJob") - .addConstraint(NetworkConstraint.KEY) - .setMaxAttempts(3) - .build()); - } - - private UpdateApkJob(@NonNull Job.Parameters parameters) { - super(parameters); - } - - @Override - public @NonNull - Data serialize() { - return Data.EMPTY; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws IOException, PackageManager.NameNotFoundException { - if (!BuildConfig.PLAY_STORE_DISABLED) return; - - Log.i(TAG, "Checking for APK update..."); - - OkHttpClient client = new OkHttpClient(); - Request request = new Request.Builder().url(String.format("%s/latest.json", BuildConfig.NOPLAY_UPDATE_URL)).build(); - - Response response = client.newCall(request).execute(); - - if (!response.isSuccessful()) { - throw new IOException("Bad response: " + response.message()); - } - - UpdateDescriptor updateDescriptor = JsonUtil.fromJson(response.body().string(), UpdateDescriptor.class); - byte[] digest = Hex.fromStringCondensed(updateDescriptor.getDigest()); - - Log.i(TAG, "Got descriptor: " + updateDescriptor); - - if (updateDescriptor.getVersionCode() > getVersionCode()) { - DownloadStatus downloadStatus = getDownloadStatus(updateDescriptor.getUrl(), digest); - - Log.i(TAG, "Download status: " + downloadStatus.getStatus()); - - if (downloadStatus.getStatus() == DownloadStatus.Status.COMPLETE) { - Log.i(TAG, "Download status complete, notifying..."); - handleDownloadNotify(downloadStatus.getDownloadId()); - } else if (downloadStatus.getStatus() == DownloadStatus.Status.MISSING) { - Log.i(TAG, "Download status missing, starting download..."); - handleDownloadStart(updateDescriptor.getUrl(), updateDescriptor.getVersionName(), digest); - } - } - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - return e instanceof IOException; - } - - @Override - public void onCanceled() { - Log.w(TAG, "Update check failed"); - } - - private int getVersionCode() throws PackageManager.NameNotFoundException { - PackageManager packageManager = context.getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); - - return packageInfo.versionCode; - } - - private DownloadStatus getDownloadStatus(String uri, byte[] theirDigest) { - DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Query query = new DownloadManager.Query(); - - query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING | DownloadManager.STATUS_SUCCESSFUL); - - long pendingDownloadId = TextSecurePreferences.getUpdateApkDownloadId(context); - byte[] pendingDigest = getPendingDigest(context); - Cursor cursor = downloadManager.query(query); - - try { - DownloadStatus status = new DownloadStatus(DownloadStatus.Status.MISSING, -1); - - while (cursor != null && cursor.moveToNext()) { - int jobStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)); - String jobRemoteUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_URI)); - long downloadId = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)); - byte[] digest = getDigestForDownloadId(downloadId); - - if (jobRemoteUri != null && jobRemoteUri.equals(uri) && downloadId == pendingDownloadId) { - - if (jobStatus == DownloadManager.STATUS_SUCCESSFUL && - digest != null && pendingDigest != null && - MessageDigest.isEqual(pendingDigest, theirDigest) && - MessageDigest.isEqual(digest, theirDigest)) - { - return new DownloadStatus(DownloadStatus.Status.COMPLETE, downloadId); - } else if (jobStatus != DownloadManager.STATUS_SUCCESSFUL) { - status = new DownloadStatus(DownloadStatus.Status.PENDING, downloadId); - } - } - } - - return status; - } finally { - if (cursor != null) cursor.close(); - } - } - - private void handleDownloadStart(String uri, String versionName, byte[] digest) { - DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(uri)); - - downloadRequest.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); - downloadRequest.setTitle("Downloading Signal update"); - downloadRequest.setDescription("Downloading Signal " + versionName); - downloadRequest.setVisibleInDownloadsUi(false); - downloadRequest.setDestinationInExternalFilesDir(context, null, "signal-update.apk"); - downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN); - - long downloadId = downloadManager.enqueue(downloadRequest); - TextSecurePreferences.setUpdateApkDownloadId(context, downloadId); - TextSecurePreferences.setUpdateApkDigest(context, Hex.toStringCondensed(digest)); - } - - private void handleDownloadNotify(long downloadId) { - Intent intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId); - - new UpdateApkReadyListener().onReceive(context, intent); - } - - private @Nullable byte[] getDigestForDownloadId(long downloadId) { - try { - DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - FileInputStream fin = new FileInputStream(downloadManager.openDownloadedFile(downloadId).getFileDescriptor()); - byte[] digest = FileUtils.getFileDigest(fin); - - fin.close(); - - return digest; - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } - - private @Nullable byte[] getPendingDigest(Context context) { - try { - String encodedDigest = TextSecurePreferences.getUpdateApkDigest(context); - - if (encodedDigest == null) return null; - - return Hex.fromStringCondensed(encodedDigest); - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } - - private static class UpdateDescriptor { - @JsonProperty - private int versionCode; - - @JsonProperty - private String versionName; - - @JsonProperty - private String url; - - @JsonProperty - private String sha256sum; - - - public int getVersionCode() { - return versionCode; - } - - public String getVersionName() { - return versionName; - } - - public String getUrl() { - return url; - } - - public @NonNull String toString() { - return "[" + versionCode + ", " + versionName + ", " + url + "]"; - } - - public String getDigest() { - return sha256sum; - } - } - - private static class DownloadStatus { - enum Status { - PENDING, - COMPLETE, - MISSING - } - - private final Status status; - private final long downloadId; - - DownloadStatus(Status status, long downloadId) { - this.status = status; - this.downloadId = downloadId; - } - - public Status getStatus() { - return status; - } - - public long getDownloadId() { - return downloadId; - } - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull UpdateApkJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new UpdateApkJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt index 07da14b09..a85ea525a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt @@ -154,7 +154,7 @@ class KeyboardPageSearchView @JvmOverloads constructor( .setDuration(REVEAL_DURATION) .alpha(0f) .setListener(object : AnimationCompleteListener() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { visibility = INVISIBLE } }) diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 697f6718c..6dcc928c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.linkpreview; +import static org.session.libsession.utilities.Util.readFully; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -8,8 +10,6 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.gms.common.util.IOUtils; - import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment; @@ -148,7 +148,7 @@ public class LinkPreviewRepository { InputStream bodyStream = response.body().byteStream(); controller.setStream(bodyStream); - byte[] data = IOUtils.readInputStreamFully(bodyStream); + byte[] data = readFully(bodyStream); Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); Optional thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaTypes.IMAGE_JPEG); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java b/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java index f0c083ca1..909f19e08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.logging; +import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; + import androidx.annotation.NonNull; import org.session.libsession.utilities.Conversions; @@ -66,15 +68,17 @@ class LogFile { byte[] plaintext = entry.getBytes(); try { - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); + synchronized (CIPHER_LOCK) { + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); - int cipherLength = cipher.getOutputSize(plaintext.length); - byte[] ciphertext = ciphertextBuffer.get(cipherLength); - cipherLength = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); + int cipherLength = cipher.getOutputSize(plaintext.length); + byte[] ciphertext = ciphertextBuffer.get(cipherLength); + cipherLength = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); - outputStream.write(ivBuffer); - outputStream.write(Conversions.intToByteArray(cipherLength)); - outputStream.write(ciphertext, 0, cipherLength); + outputStream.write(ivBuffer); + outputStream.write(Conversions.intToByteArray(cipherLength)); + outputStream.write(ciphertext, 0, cipherLength); + } outputStream.flush(); } catch (ShortBufferException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { @@ -134,10 +138,11 @@ class LogFile { Util.readFully(inputStream, ciphertext, length); try { - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); - byte[] plaintext = cipher.doFinal(ciphertext, 0, length); - - return new String(plaintext); + synchronized (CIPHER_LOCK) { + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); + byte[] plaintext = cipher.doFinal(ciphertext, 0, length); + return new String(plaintext); + } } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index eac40f681..cba1529a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiEventListener; import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider; import org.thoughtcrime.securesms.components.emoji.EmojiToggle; import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; -import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; +import org.thoughtcrime.securesms.util.SimpleTextWatcher; import org.thoughtcrime.securesms.imageeditor.model.EditorModel; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt index 9a8d06129..af3d269c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt @@ -34,7 +34,6 @@ class MessageRequestView : LinearLayout { // region Updating fun bind(thread: ThreadRecord, glide: GlideRequests) { this.thread = thread - binding.profilePictureView.root.glide = glide val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() binding.displayNameTextView.text = senderDisplayName @@ -44,12 +43,12 @@ class MessageRequestView : LinearLayout { binding.snippetTextView.text = snippet post { - binding.profilePictureView.root.update(thread.recipient) + binding.profilePictureView.update(thread.recipient) } } fun recycle() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() } private fun getUserDisplayName(recipient: Recipient): String? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt index 50ed4628e..caecbcd87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.messagerequests -import android.app.AlertDialog import android.content.Intent import android.database.Cursor import android.os.Bundle @@ -20,6 +19,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.push import javax.inject.Inject @@ -49,7 +49,7 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat adapter.glide = glide binding.recyclerView.adapter = adapter - binding.clearAllMessageRequestsButton.setOnClickListener { deleteAllAndBlock() } + binding.clearAllMessageRequestsButton.setOnClickListener { deleteAll() } } override fun onResume() { @@ -77,34 +77,34 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat } override fun onBlockConversationClick(thread: ThreadRecord) { - val dialog = AlertDialog.Builder(this) - dialog.setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question) - .setMessage(R.string.message_requests_block_message) - .setPositiveButton(R.string.recipient_preferences__block) { _, _ -> - viewModel.blockMessageRequest(thread) - LoaderManager.getInstance(this).restartLoader(0, null, this) - } - .setNegativeButton(R.string.no) { _, _ -> - // Do nothing - } - dialog.create().show() + fun doBlock() { + viewModel.blockMessageRequest(thread) + LoaderManager.getInstance(this).restartLoader(0, null, this) + } + + showSessionDialog { + title(R.string.RecipientPreferenceActivity_block_this_contact_question) + text(R.string.message_requests_block_message) + button(R.string.recipient_preferences__block) { doBlock() } + button(R.string.no) + } } override fun onDeleteConversationClick(thread: ThreadRecord) { - val dialog = AlertDialog.Builder(this) - dialog.setTitle(R.string.decline) - .setMessage(resources.getString(R.string.message_requests_decline_message)) - .setPositiveButton(R.string.decline) { _,_ -> - viewModel.deleteMessageRequest(thread) - LoaderManager.getInstance(this).restartLoader(0, null, this) - lifecycleScope.launch(Dispatchers.IO) { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity) - } + fun doDecline() { + viewModel.deleteMessageRequest(thread) + LoaderManager.getInstance(this).restartLoader(0, null, this) + lifecycleScope.launch(Dispatchers.IO) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity) } - .setNegativeButton(R.string.no) { _, _ -> - // Do nothing - } - dialog.create().show() + } + + showSessionDialog { + title(R.string.decline) + text(resources.getString(R.string.message_requests_decline_message)) + button(R.string.decline) { doDecline() } + button(R.string.no) + } } private fun updateEmptyState() { @@ -113,19 +113,19 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat binding.clearAllMessageRequestsButton.isVisible = threadCount != 0 } - private fun deleteAllAndBlock() { - val dialog = AlertDialog.Builder(this) - dialog.setMessage(resources.getString(R.string.message_requests_clear_all_message)) - dialog.setPositiveButton(R.string.yes) { _, _ -> - viewModel.clearAllMessageRequests() + private fun deleteAll() { + fun doDeleteAllAndBlock() { + viewModel.clearAllMessageRequests(false) LoaderManager.getInstance(this).restartLoader(0, null, this) lifecycleScope.launch(Dispatchers.IO) { ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity) } } - dialog.setNegativeButton(R.string.no) { _, _ -> - // Do nothing + + showSessionDialog { + text(resources.getString(R.string.message_requests_clear_all_message)) + button(R.string.yes) { doDeleteAllAndBlock() } + button(R.string.no) } - dialog.create().show() } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt index 89a841dc0..10142cc8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt @@ -48,6 +48,7 @@ class MessageRequestsAdapter( private fun showPopupMenu(view: MessageRequestView) { val popupMenu = PopupMenu(ContextThemeWrapper(context, R.style.PopupMenu_MessageRequests), view) popupMenu.menuInflater.inflate(R.menu.menu_message_request, popupMenu.menu) + popupMenu.menu.findItem(R.id.menu_block_message_request)?.isVisible = !view.thread!!.recipient.isOpenGroupInboxRecipient popupMenu.setOnMenuItemClickListener { menuItem -> if (menuItem.itemId == R.id.menu_delete_message_request) { listener.onDeleteConversationClick(view.thread!!) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt index 2f448932d..a3a7caf8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt @@ -25,8 +25,8 @@ class MessageRequestsViewModel @Inject constructor( repository.deleteMessageRequest(thread) } - fun clearAllMessageRequests() = viewModelScope.launch { - repository.clearAllMessageRequests() + fun clearAllMessageRequests(block: Boolean) = viewModelScope.launch { + repository.clearAllMessageRequests(block) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java index 179c28bc3..22af450aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java @@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints { @Override public int getImageMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } @Override public int getGifMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } @Override public int getVideoMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } @Override public int getAudioMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } @Override public int getDocumentMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index 884d6d7be..ddff9f52b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -30,6 +30,7 @@ import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsession.messaging.messages.visible.VisibleMessage; 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.recipients.Recipient; import org.session.libsignal.utilities.Log; @@ -82,7 +83,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { VisibleMessage message = new VisibleMessage(); message.setText(responseText.toString()); - message.setSentTimestamp(System.currentTimeMillis()); + message.setSentTimestamp(SnodeAPI.getNowWithOffset()); MessageSender.send(message, recipient.getAddress()); if (recipient.isGroupRecipient()) { @@ -96,7 +97,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { } else { Log.w("AndroidAutoReplyReceiver", "Sending regular message "); OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient); - DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null, true); + DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, SnodeAPI.getNowWithOffset(), null, true); } List messageIds = DatabaseComponent.get(context).threadDatabase().setRead(replyThreadId, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt index 5a0438e15..e583fb0ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt @@ -4,9 +4,11 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.work.Constraints +import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters @@ -21,19 +23,35 @@ import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPolle import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.recover import org.thoughtcrime.securesms.dependencies.DatabaseComponent import java.util.concurrent.TimeUnit class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { + enum class Targets { + DMS, CLOSED_GROUPS, OPEN_GROUPS + } companion object { const val TAG = "BackgroundPollWorker" + const val INITIAL_SCHEDULE_TIME = "INITIAL_SCHEDULE_TIME" + const val REQUEST_TARGETS = "REQUEST_TARGETS" @JvmStatic - fun schedulePeriodic(context: Context) { + fun schedulePeriodic(context: Context) = schedulePeriodic(context, targets = Targets.values()) + + @JvmStatic + fun schedulePeriodic(context: Context, targets: Array) { Log.v(TAG, "Scheduling periodic work.") - val builder = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) + val durationMinutes: Long = 15 + val builder = PeriodicWorkRequestBuilder(durationMinutes, TimeUnit.MINUTES) builder.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + + val dataBuilder = Data.Builder() + dataBuilder.putLong(INITIAL_SCHEDULE_TIME, System.currentTimeMillis() + (durationMinutes * 60 * 1000)) + dataBuilder.putStringArray(REQUEST_TARGETS, targets.map { it.name }.toTypedArray()) + builder.setInputData(dataBuilder.build()) + val workRequest = builder.build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( TAG, @@ -41,6 +59,20 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor workRequest ) } + + @JvmStatic + fun scheduleOnce(context: Context, targets: Array = Targets.values()) { + Log.v(TAG, "Scheduling single run.") + val builder = OneTimeWorkRequestBuilder() + builder.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + + val dataBuilder = Data.Builder() + dataBuilder.putStringArray(REQUEST_TARGETS, targets.map { it.name }.toTypedArray()) + builder.setInputData(dataBuilder.build()) + + val workRequest = builder.build() + WorkManager.getInstance(context).enqueue(workRequest) + } } override fun doWork(): Result { @@ -49,41 +81,89 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor return Result.failure() } + // If this is a scheduled run and it is happening before the initial scheduled time (as + // periodic background tasks run immediately when scheduled) then don't actually do anything + // because this might slow requests on initial startup or triggered by PNs + val initialScheduleTime = inputData.getLong(INITIAL_SCHEDULE_TIME, -1) + + if (initialScheduleTime != -1L && System.currentTimeMillis() < (initialScheduleTime - (60 * 1000))) { + Log.v(TAG, "Skipping initial run.") + return Result.success() + } + + // Retrieve the desired targets (defaulting to all if not provided or empty) + val requestTargets: List = (inputData.getStringArray(REQUEST_TARGETS) ?: emptyArray()) + .map { + try { Targets.valueOf(it) } + catch(e: Exception) { null } + } + .filterNotNull() + .ifEmpty { Targets.values().toList() } + try { - Log.v(TAG, "Performing background poll.") + Log.v(TAG, "Performing background poll for ${requestTargets.joinToString { it.name }}.") val promises = mutableListOf>() // DMs - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val dmsPromise = SnodeAPI.getMessages(userPublicKey).bind { envelopes -> - val params = envelopes.map { (envelope, serverHash) -> - // FIXME: Using a job here seems like a bad idea... - MessageReceiveParameters(envelope.toByteArray(), serverHash, null) + var dmsPromise: Promise = Promise.ofSuccess(Unit) + + if (requestTargets.contains(Targets.DMS)) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + dmsPromise = SnodeAPI.getMessages(userPublicKey).bind { envelopes -> + val params = envelopes.map { (envelope, serverHash) -> + // FIXME: Using a job here seems like a bad idea... + MessageReceiveParameters(envelope.toByteArray(), serverHash, null) + } + BatchMessageReceiveJob(params).executeAsync("background") } - BatchMessageReceiveJob(params).executeAsync() + promises.add(dmsPromise) } - promises.add(dmsPromise) // Closed groups - val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared - val storage = MessagingModuleConfiguration.shared.storage - val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys() - allGroupPublicKeys.iterator().forEach { closedGroupPoller.poll(it) } + if (requestTargets.contains(Targets.CLOSED_GROUPS)) { + val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared + val storage = MessagingModuleConfiguration.shared.storage + val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys() + allGroupPublicKeys.iterator().forEach { closedGroupPoller.poll(it) } + } // Open Groups - val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() - val openGroups = threadDB.getAllOpenGroups() - val openGroupServers = openGroups.map { it.value.server }.toSet() + var ogPollError: Exception? = null - for (server in openGroupServers) { - val poller = OpenGroupPoller(server, null) - poller.hasStarted = true - promises.add(poller.poll()) + if (requestTargets.contains(Targets.OPEN_GROUPS)) { + val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() + val openGroups = threadDB.getAllOpenGroups() + val openGroupServers = openGroups.map { it.value.server }.toSet() + + for (server in openGroupServers) { + val poller = OpenGroupPoller(server, null) + poller.hasStarted = true + + // If one of the open group pollers fails we don't want it to cancel the DM + // poller so just hold on to the error for later + promises.add( + poller.poll().recover { + if (dmsPromise.isDone()) { + throw it + } + + ogPollError = it + } + ) + } } // Wait until all the promises are resolved all(promises).get() + // If the Open Group pollers threw an exception then re-throw it here (now that + // the DM promise has completed) + val localOgPollException = ogPollError + + if (localOgPollException != null) { + throw localOgPollException + } + return Result.success() } catch (exception: Exception) { Log.e(TAG, "Background poll failed due to error: ${exception.message}.", exception) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 728462903..2c70bff63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -44,6 +44,7 @@ import org.session.libsession.messaging.open_groups.OpenGroup; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.utilities.SessionId; import org.session.libsession.messaging.utilities.SodiumUtilities; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Contact; import org.session.libsession.utilities.ServiceUtil; @@ -53,13 +54,12 @@ import org.session.libsignal.utilities.IdPrefix; import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Util; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.contactshare.ContactUtil; +import org.thoughtcrime.securesms.contacts.ContactUtil; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities; import org.thoughtcrime.securesms.crypto.KeyPairUtilities; import org.thoughtcrime.securesms.database.LokiThreadDatabase; -import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; @@ -137,7 +137,7 @@ public class DefaultMessageNotifier implements MessageNotifier { Intent intent = new Intent(context, ConversationActivityV2.class); intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress()); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); - intent.setData((Uri.parse("custom://" + System.currentTimeMillis()))); + intent.setData((Uri.parse("custom://" + SnodeAPI.getNowWithOffset()))); FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent); ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) @@ -159,8 +159,9 @@ public class DefaultMessageNotifier implements MessageNotifier { executor.cancel(); } - private void cancelActiveNotifications(@NonNull Context context) { + private boolean cancelActiveNotifications(@NonNull Context context) { NotificationManager notifications = ServiceUtil.getNotificationManager(context); + boolean hasNotifications = notifications.getActiveNotifications().length > 0; notifications.cancel(SUMMARY_NOTIFICATION_ID); try { @@ -174,6 +175,7 @@ public class DefaultMessageNotifier implements MessageNotifier { Log.w(TAG, e); notifications.cancelAll(); } + return hasNotifications; } private void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { @@ -239,10 +241,6 @@ public class DefaultMessageNotifier implements MessageNotifier { !(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) { TextSecurePreferences.removeHasHiddenMessageRequests(context); } - if (isVisible && recipient != null) { - List messageIds = threads.setRead(threadId, false); - if (SessionMetaProtocol.shouldSendReadReceipt(recipient)) { MarkReadReceiver.process(context, messageIds); } - } if (!TextSecurePreferences.isNotificationsEnabled(context) || (recipient != null && recipient.isMuted())) @@ -250,11 +248,21 @@ public class DefaultMessageNotifier implements MessageNotifier { return; } - if (!isVisible && !homeScreenVisible) { + if ((!isVisible && !homeScreenVisible) || hasExistingNotifications(context)) { updateNotification(context, signal, 0); } } + private boolean hasExistingNotifications(Context context) { + NotificationManager notifications = ServiceUtil.getNotificationManager(context); + try { + StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); + return activeNotifications.length > 0; + } catch (Exception e) { + return false; + } + } + @Override public void updateNotification(@NonNull Context context, boolean signal, int reminderCount) { @@ -266,8 +274,8 @@ public class DefaultMessageNotifier implements MessageNotifier { if ((telcoCursor == null || telcoCursor.isAfterLast()) || !TextSecurePreferences.hasSeenWelcomeScreen(context)) { - cancelActiveNotifications(context); updateBadge(context, 0); + cancelActiveNotifications(context); clearReminder(context); return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt deleted file mode 100644 index 87a9efc0d..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt +++ /dev/null @@ -1,19 +0,0 @@ -@file:JvmName("FcmUtils") -package org.thoughtcrime.securesms.notifications - -import com.google.android.gms.tasks.Task -import com.google.firebase.iid.FirebaseInstanceId -import com.google.firebase.iid.InstanceIdResult -import kotlinx.coroutines.* - - -fun getFcmInstanceId(body: (Task)->Unit): Job = MainScope().launch(Dispatchers.IO) { - val task = FirebaseInstanceId.getInstance().instanceId - while (!task.isComplete && isActive) { - // wait for task to complete while we are active - } - if (!isActive) return@launch // don't 'complete' task if we were canceled - withContext(Dispatchers.Main) { - body(task) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt deleted file mode 100644 index adaec0e17..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt +++ /dev/null @@ -1,121 +0,0 @@ -package org.thoughtcrime.securesms.notifications - -import android.content.Context -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.map -import okhttp3.MediaType -import okhttp3.Request -import okhttp3.RequestBody -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI -import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.snode.Version -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.retryIfNeeded -import org.thoughtcrime.securesms.dependencies.DatabaseComponent - -object LokiPushNotificationManager { - private val maxRetryCount = 4 - private val tokenExpirationInterval = 12 * 60 * 60 * 1000 - - private val server by lazy { - PushNotificationAPI.server - } - private val pnServerPublicKey by lazy { - PushNotificationAPI.serverPublicKey - } - - enum class ClosedGroupOperation { - Subscribe, Unsubscribe; - - val rawValue: String - get() { - return when (this) { - Subscribe -> "subscribe_closed_group" - Unsubscribe -> "unsubscribe_closed_group" - } - } - } - - @JvmStatic - fun unregister(token: String, context: Context) { - val parameters = mapOf( "token" to token ) - val url = "$server/unregister" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { json -> - val code = json["code"] as? Int - if (code != null && code != 0) { - TextSecurePreferences.setIsUsingFCM(context, false) - } else { - Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.") - } - }.fail { exception -> - Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.") - } - } - // Unsubscribe from all closed groups - val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys() - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - allClosedGroupPublicKeys.iterator().forEach { closedGroup -> - performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) - } - } - - @JvmStatic - fun register(token: String, publicKey: String, context: Context, force: Boolean) { - val oldToken = TextSecurePreferences.getFCMToken(context) - val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) - if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return } - val parameters = mapOf( "token" to token, "pubKey" to publicKey ) - val url = "$server/register" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { json -> - val code = json["code"] as? Int - if (code != null && code != 0) { - TextSecurePreferences.setIsUsingFCM(context, true) - TextSecurePreferences.setFCMToken(context, token) - TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) - } else { - Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.") - } - }.fail { exception -> - Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.") - } - } - // Subscribe to all closed groups - val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys() - allClosedGroupPublicKeys.iterator().forEach { closedGroup -> - performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey) - } - } - - @JvmStatic - fun performOperation(context: Context, operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) { - if (!TextSecurePreferences.isUsingFCM(context)) { return } - val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) - val url = "$server/${operation.rawValue}" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { json -> - val code = json["code"] as? Int - if (code == null || code == 0) { - Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.") - } - }.fail { exception -> - Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.") - } - } - } - - private fun getResponseBody(request: Request): Promise, Exception> { - return OnionRequestAPI.sendOnionRequest(request, server, pnServerPublicKey, Version.V2).map { response -> - JsonUtil.fromJson(response.body, Map::class.java) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index f1ec6d188..309f2732f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -12,8 +12,11 @@ import androidx.core.app.NotificationManagerCompat; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; +import org.session.libsession.database.StorageProtocol; +import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.messages.control.ReadReceipt; 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.TextSecurePreferences; import org.session.libsession.utilities.recipients.Recipient; @@ -26,7 +29,6 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.util.SessionMetaProtocol; -import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -51,18 +53,12 @@ public class MarkReadReceiver extends BroadcastReceiver { new AsyncTask() { @Override protected Void doInBackground(Void... params) { - List messageIdsCollection = new LinkedList<>(); - + long currentTime = SnodeAPI.getNowWithOffset(); for (long threadId : threadIds) { Log.i(TAG, "Marking as read: " + threadId); - List messageIds = DatabaseComponent.get(context).threadDatabase().setRead(threadId, true); - messageIdsCollection.addAll(messageIds); + StorageProtocol storage = MessagingModuleConfiguration.getShared().getStorage(); + storage.markConversationAsRead(threadId,currentTime, true); } - - process(context, messageIdsCollection); - - ApplicationContext.getInstance(context).messageNotifier.updateNotification(context); - return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -86,7 +82,7 @@ public class MarkReadReceiver extends BroadcastReceiver { List timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList(); if (!SessionMetaProtocol.shouldSendReadReceipt(Recipient.from(context, address, false))) { continue; } ReadReceipt readReceipt = new ReadReceipt(timestamps); - readReceipt.setSentTimestamp(System.currentTimeMillis()); + readReceipt.setSentTimestamp(SnodeAPI.getNowWithOffset()); MessageSender.send(readReceipt, address); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt new file mode 100644 index 000000000..d094644c0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt @@ -0,0 +1,5 @@ +package org.thoughtcrime.securesms.notifications + +interface PushManager { + fun refresh(force: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt deleted file mode 100644 index fc399d293..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.thoughtcrime.securesms.notifications - -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage -import org.session.libsession.messaging.jobs.BatchMessageReceiveJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.utilities.MessageWrapper -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.Log - -class PushNotificationService : FirebaseMessagingService() { - - override fun onNewToken(token: String) { - super.onNewToken(token) - Log.d("Loki", "New FCM token: $token.") - val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return - LokiPushNotificationManager.register(token, userPublicKey, this, false) - } - - override fun onMessageReceived(message: RemoteMessage) { - Log.d("Loki", "Received a push notification.") - val base64EncodedData = message.data?.get("ENCRYPTED_DATA") - val data = base64EncodedData?.let { Base64.decode(it) } - if (data != null) { - try { - val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() - val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) - JobQueue.shared.add(job) - } catch (e: Exception) { - Log.d("Loki", "Failed to unwrap data for message due to error: $e.") - } - } else { - Log.d("Loki", "Failed to decode data for message.") - val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER) - .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) - .setColor(this.getResources().getColor(network.loki.messenger.R.color.textsecure_primary)) - .setContentTitle("Session") - .setContentText("You've got a new message.") - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) - with(NotificationManagerCompat.from(this)) { - notify(11111, builder.build()) - } - } - } - - override fun onDeletedMessages() { - Log.d("Loki", "Called onDeletedMessages.") - super.onDeletedMessages() - val token = TextSecurePreferences.getFCMToken(this)!! - val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return - LokiPushNotificationManager.register(token, userPublicKey, this, true) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt new file mode 100644 index 000000000..398211398 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -0,0 +1,114 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.interfaces.AEAD +import com.goterl.lazysodium.utils.Key +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.session.libsession.messaging.jobs.BatchMessageReceiveJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.messaging.utilities.SodiumUtilities +import org.session.libsession.utilities.bencode.Bencode +import org.session.libsession.utilities.bencode.BencodeList +import org.session.libsession.utilities.bencode.BencodeString +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import javax.inject.Inject + +private const val TAG = "PushHandler" + +class PushReceiver @Inject constructor(@ApplicationContext val context: Context) { + private val sodium = LazySodiumAndroid(SodiumAndroid()) + + fun onPush(dataMap: Map?) { + onPush(dataMap?.asByteArray()) + } + + fun onPush(data: ByteArray?) { + if (data == null) { + onPush() + return + } + + try { + val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() + val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) + JobQueue.shared.add(job) + } catch (e: Exception) { + Log.d(TAG, "Failed to unwrap data for message due to error.", e) + } + } + + private fun onPush() { + Log.d(TAG, "Failed to decode data for message.") + val builder = NotificationCompat.Builder(context, NotificationChannels.OTHER) + .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) + .setColor(context.getColor(network.loki.messenger.R.color.textsecure_primary)) + .setContentTitle("Session") + .setContentText("You've got a new message.") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + NotificationManagerCompat.from(context).notify(11111, builder.build()) + } + + private fun Map.asByteArray() = + when { + // this is a v2 push notification + containsKey("spns") -> { + try { + decrypt(Base64.decode(this["enc_payload"])) + } catch (e: Exception) { + Log.e(TAG, "Invalid push notification", e) + null + } + } + // old v1 push notification; we still need this for receiving legacy closed group notifications + else -> this["ENCRYPTED_DATA"]?.let(Base64::decode) + } + + private fun decrypt(encPayload: ByteArray): ByteArray? { + Log.d(TAG, "decrypt() called") + + val encKey = getOrCreateNotificationKey() + val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) + ?: error("Failed to decrypt push notification") + val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray() + val bencoded = Bencode.Decoder(decrypted) + val expectedList = (bencoded.decode() as? BencodeList)?.values + ?: error("Failed to decode bencoded list from payload") + + val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") + val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) + + return (expectedList.getOrNull(1) as? BencodeString)?.value.also { + // null content is valid only if we got a "data_too_long" flag + it?.let { check(metadata.data_len == it.size) { "wrong message data size" } } + ?: check(metadata.data_too_long) { "missing message data, but no too-long flag" } + } + } + + fun getOrCreateNotificationKey(): Key { + if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) { + // generate the key and store it + val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) + IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) + } + return Key.fromHexString( + IdentityKeyUtil.retrieve( + context, + IdentityKeyUtil.NOTIFICATION_KEY + ) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt new file mode 100644 index 000000000..b0954f232 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import com.goterl.lazysodium.utils.KeyPair +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.combine.and +import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 +import org.session.libsession.utilities.Device +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Namespace +import org.session.libsignal.utilities.emptyPromise +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import javax.inject.Inject +import javax.inject.Singleton + +private val TAG = PushRegistry::class.java.name + +@Singleton +class PushRegistry @Inject constructor( + @ApplicationContext private val context: Context, + private val device: Device, + private val tokenManager: TokenManager, + private val pushRegistryV2: PushRegistryV2, + private val prefs: TextSecurePreferences, + private val tokenFetcher: TokenFetcher, +) { + + private var pushRegistrationJob: Job? = null + + fun refresh(force: Boolean): Job { + Log.d(TAG, "refresh() called with: force = $force") + + pushRegistrationJob?.apply { + if (force) cancel() else if (isActive) return MainScope().launch {} + } + + return MainScope().launch(Dispatchers.IO) { + try { + register(tokenFetcher.fetch()).get() + } catch (e: Exception) { + Log.e(TAG, "register failed", e) + } + }.also { pushRegistrationJob = it } + } + + fun register(token: String?): Promise<*, Exception> { + Log.d(TAG, "refresh() called") + + if (token?.isNotEmpty() != true) return emptyPromise() + + prefs.setPushToken(token) + + val userPublicKey = prefs.getLocalNumber() ?: return emptyPromise() + val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() + + return when { + prefs.isPushEnabled() -> register(token, userPublicKey, userEdKey) + tokenManager.isRegistered -> unregister(token, userPublicKey, userEdKey) + else -> emptyPromise() + } + } + + /** + * Register for push notifications. + */ + private fun register( + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List = listOf(Namespace.DEFAULT) + ): Promise<*, Exception> { + Log.d(TAG, "register() called") + + val v1 = PushRegistryV1.register( + device = device, + token = token, + publicKey = publicKey + ) fail { + Log.e(TAG, "register v1 failed", it) + } + + val v2 = pushRegistryV2.register( + device, token, publicKey, userEd25519Key, namespaces + ) fail { + Log.e(TAG, "register v2 failed", it) + } + + return v1 and v2 success { + Log.d(TAG, "register v1 & v2 success") + tokenManager.register() + } + } + + private fun unregister( + token: String, + userPublicKey: String, + userEdKey: KeyPair + ): Promise<*, Exception> = PushRegistryV1.unregister() and pushRegistryV2.unregister( + device, token, userPublicKey, userEdKey + ) fail { + Log.e(TAG, "unregisterBoth failed", it) + } success { + tokenManager.unregister() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt new file mode 100644 index 000000000..4bef45ff9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt @@ -0,0 +1,116 @@ +package org.thoughtcrime.securesms.notifications + +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.interfaces.Sign +import com.goterl.lazysodium.utils.KeyPair +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.map +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody +import org.session.libsession.messaging.sending_receiving.notifications.Response +import org.session.libsession.messaging.sending_receiving.notifications.Server +import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest +import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest +import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.Version +import org.session.libsession.utilities.Device +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Namespace +import org.session.libsignal.utilities.retryIfNeeded +import javax.inject.Inject +import javax.inject.Singleton + +private val TAG = PushRegistryV2::class.java.name +private const val maxRetryCount = 4 + +@Singleton +class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) { + private val sodium = LazySodiumAndroid(SodiumAndroid()) + + fun register( + device: Device, + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List + ): Promise { + val pnKey = pushReceiver.getOrCreateNotificationKey() + + val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s + // if we want to support passing namespace list, here is the place to do it + val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes) + val requestParameters = SubscriptionRequest( + pubkey = publicKey, + session_ed25519 = userEd25519Key.publicKey.asHexString, + namespaces = listOf(Namespace.DEFAULT), + data = true, // only permit data subscription for now (?) + service = device.service, + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + enc_key = pnKey.asHexString, + ).let(Json::encodeToString) + + return retryResponseBody("subscribe", requestParameters) success { + Log.d(TAG, "registerV2 success") + } + } + + fun unregister( + device: Device, + token: String, + userPublicKey: String, + userEdKey: KeyPair + ): Promise { + val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s + // if we want to support passing namespace list, here is the place to do it + val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) + + val requestParameters = UnsubscriptionRequest( + pubkey = userPublicKey, + session_ed25519 = userEdKey.publicKey.asHexString, + service = device.service, + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + ).let(Json::encodeToString) + + return retryResponseBody("unsubscribe", requestParameters) success { + Log.d(TAG, "unregisterV2 success") + } + } + + private inline fun retryResponseBody(path: String, requestParameters: String): Promise = + retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) } + + private inline fun getResponseBody(path: String, requestParameters: String): Promise { + val server = Server.LATEST + val url = "${server.url}/$path" + val body = RequestBody.create(MediaType.get("application/json"), requestParameters) + val request = Request.Builder().url(url).post(body).build() + + return OnionRequestAPI.sendOnionRequest( + request, + server.url, + server.publicKey, + Version.V4 + ).map { response -> + response.body!!.inputStream() + .let { Json.decodeFromStream(it) } + .also { if (it.isFailure()) throw Exception("error: ${it.message}.") } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index da0896d05..891d0bb2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -99,7 +99,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil .get(); setLargeIcon(iconBitmap); } catch (InterruptedException | ExecutionException e) { - Log.w(TAG, e); + Log.w(TAG, "get iconBitmap in getThread failed", e); setLargeIcon(getPlaceholderDrawable(context, recipient)); } } else { @@ -298,7 +298,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil .submit(64, 64) .get(); } catch (InterruptedException | ExecutionException e) { - Log.w(TAG, e); + Log.w(TAG, "getBigPicture failed", e); return Bitmap.createBitmap(64, 64, Bitmap.Config.RGB_565); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt new file mode 100644 index 000000000..5bd9ce0d8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt @@ -0,0 +1,5 @@ +package org.thoughtcrime.securesms.notifications + +interface TokenFetcher { + suspend fun fetch(): String? +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenManager.kt new file mode 100644 index 000000000..b3db642b8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenManager.kt @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import org.session.libsession.utilities.TextSecurePreferences +import javax.inject.Inject +import javax.inject.Singleton + +private const val INTERVAL: Int = 12 * 60 * 60 * 1000 + +@Singleton +class TokenManager @Inject constructor( + @ApplicationContext private val context: Context, +) { + val hasValidRegistration get() = isRegistered && !isExpired + val isRegistered get() = time > 0 + private val isExpired get() = currentTime() > time + INTERVAL + + fun register() { + time = currentTime() + } + + fun unregister() { + time = 0 + } + + private var time + get() = TextSecurePreferences.getPushRegisterTime(context) + set(value) = TextSecurePreferences.setPushRegisterTime(context, value) + + private fun currentTime() = System.currentTimeMillis() +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index 31117ae94..edd1bc274 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentPagerAdapter import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filter @@ -34,12 +35,19 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import javax.inject.Inject +@AndroidEntryPoint class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { + + @Inject + lateinit var configFactory: ConfigFactory + private lateinit var binding: ActivityLinkDeviceBinding internal val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -112,6 +120,7 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel val keyPairGenerationResult = KeyPairUtilities.generate(seed) val x25519KeyPair = keyPairGenerationResult.x25519KeyPair KeyPairUtilities.store(this@LinkDeviceActivity, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) + configFactory.keyPairChanged() val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey val registrationID = KeyHelper.generateRegistrationId(false) TextSecurePreferences.setLocalRegistrationId(this@LinkDeviceActivity, registrationID) @@ -124,9 +133,8 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel .setAction(R.string.registration_activity__skip) { register(true) } val skipJob = launch { - delay(30_000L) + delay(15_000L) snackBar.show() - // show a dialog or something saying do you want to skip this bit? } // start polling and wait for updated message ApplicationContext.getInstance(this@LinkDeviceActivity).apply { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index 9cf9c3d04..e4e8e6a9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.onboarding import android.animation.ArgbEvaluator import android.animation.ValueAnimator -import android.app.AlertDialog import android.content.Intent import android.graphics.drawable.TransitionDrawable import android.net.Uri @@ -13,6 +12,7 @@ import android.view.View import android.widget.Toast import androidx.annotation.ColorInt import androidx.annotation.DrawableRes +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ActivityPnModeBinding import org.session.libsession.utilities.TextSecurePreferences @@ -20,6 +20,9 @@ import org.session.libsession.utilities.ThemeUtil import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.home.HomeActivity +import org.thoughtcrime.securesms.notifications.PushManager +import org.thoughtcrime.securesms.notifications.PushRegistry +import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.PNModeView import org.thoughtcrime.securesms.util.disableClipping @@ -27,8 +30,13 @@ import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getColorWithID import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.thoughtcrime.securesms.util.show +import javax.inject.Inject +@AndroidEntryPoint class PNModeActivity : BaseActionBarActivity() { + + @Inject lateinit var pushRegistry: PushRegistry + private lateinit var binding: ActivityPnModeBinding private var selectedOptionView: PNModeView? = null @@ -151,18 +159,20 @@ class PNModeActivity : BaseActionBarActivity() { private fun register() { if (selectedOptionView == null) { - val dialog = AlertDialog.Builder(this) - dialog.setTitle(R.string.activity_pn_mode_no_option_picked_dialog_title) - dialog.setPositiveButton(R.string.ok) { _, _ -> } - dialog.create().show() + showSessionDialog { + title(R.string.activity_pn_mode_no_option_picked_dialog_title) + button(R.string.ok) + } return } - TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView)) + + TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView)) val application = ApplicationContext.getInstance(this) application.startPollingIfNeeded() - application.registerForFCMIfNeeded(true) + pushRegistry.refresh(true) val intent = Intent(this, HomeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + intent.putExtra(HomeActivity.FROM_ONBOARDING, true) show(intent) } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt index 5531fea49..051cd7542 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt @@ -11,6 +11,7 @@ import android.text.style.ClickableSpan import android.text.style.StyleSpan import android.view.View import android.widget.Toast +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding import org.session.libsession.snode.SnodeModule @@ -23,10 +24,17 @@ import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import javax.inject.Inject +@AndroidEntryPoint class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { + + @Inject + lateinit var configFactory: ConfigFactory + private lateinit var binding: ActivityRecoveryPhraseRestoreBinding internal val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -81,6 +89,7 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { val keyPairGenerationResult = KeyPairUtilities.generate(seed) val x25519KeyPair = keyPairGenerationResult.x25519KeyPair KeyPairUtilities.store(this, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) + configFactory.keyPairChanged() val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey val registrationID = KeyHelper.generateRegistrationId(false) TextSecurePreferences.setLocalRegistrationId(this, registrationID) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt index b6fdaf9cf..6e082e000 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt @@ -16,6 +16,7 @@ import android.text.style.StyleSpan import android.view.View import android.widget.Toast import com.goterl.lazysodium.utils.KeyPair +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ActivityRegisterBinding import org.session.libsession.snode.SnodeModule @@ -26,10 +27,17 @@ import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import javax.inject.Inject +@AndroidEntryPoint class RegisterActivity : BaseActionBarActivity() { + + @Inject + lateinit var configFactory: ConfigFactory + private lateinit var binding: ActivityRegisterBinding internal val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -119,6 +127,7 @@ class RegisterActivity : BaseActionBarActivity() { database.clearReceivedMessageHashValues() KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!) + configFactory.keyPairChanged() val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey val registrationID = KeyHelper.generateRegistrationId(false) TextSecurePreferences.setLocalRegistrationId(this, registrationID) diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java index 999dad001..88ee67cb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.permissions; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -12,6 +13,7 @@ import android.util.DisplayMetrics; import android.view.Display; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.Button; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; @@ -160,14 +162,13 @@ public class Permissions { request.onResult(requestedPermissions, grantResults, new boolean[requestedPermissions.length]); } - @SuppressWarnings("ConstantConditions") private void executePermissionsRequestWithRationale(PermissionsRequest request) { - RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader) - .setPositiveButton(R.string.Permissions_continue, (dialog, which) -> executePermissionsRequest(request)) - .setNegativeButton(R.string.Permissions_not_now, (dialog, which) -> executeNoPermissionsRequest(request)) - .show() - .getWindow() - .setLayout((int)(permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT); + RationaleDialog.show( + permissionObject.getContext(), + rationaleDialogMessage, + () -> executePermissionsRequest(request), + () -> executeNoPermissionsRequest(request), + rationalDialogHeader); } private void executePermissionsRequest(PermissionsRequest request) { @@ -254,7 +255,7 @@ public class Permissions { resultListener.onResult(permissions, grantResults, shouldShowRationaleDialog); } - private static Intent getApplicationSettingsIntent(@NonNull Context context) { + static Intent getApplicationSettingsIntent(@NonNull Context context) { Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", context.getPackageName(), null); @@ -351,15 +352,8 @@ public class Permissions { @Override public void run() { Context context = this.context.get(); - - if (context != null) { - new AlertDialog.Builder(context, R.style.ThemeOverlay_Session_AlertDialog) - .setTitle(R.string.Permissions_permission_required) - .setMessage(message) - .setPositiveButton(R.string.Permissions_continue, (dialog, which) -> context.startActivity(getApplicationSettingsIntent(context))) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } + if (context == null) return; + SettingsDialog.show(context, message); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java deleted file mode 100644 index a346d591a..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.thoughtcrime.securesms.permissions; - - -import android.app.AlertDialog; -import android.content.Context; -import android.graphics.Color; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout.LayoutParams; -import android.widget.TextView; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; - -import org.session.libsession.utilities.ViewUtil; - -import network.loki.messenger.R; - -public class RationaleDialog { - - public static AlertDialog.Builder createFor(@NonNull Context context, @NonNull String message, @DrawableRes int... drawables) { - View view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null); - view.setClipToOutline(true); - ViewGroup header = view.findViewById(R.id.header_container); - TextView text = view.findViewById(R.id.message); - - for (int i=0;i(R.id.header_container) + view.findViewById(R.id.message).text = message + + fun addIcon(id: Int) { + ImageView(context).apply { + setImageDrawable(ResourcesCompat.getDrawable(context.resources, id, context.theme)) + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + }.also(header::addView) + } + + fun addPlus() { + TextView(context).apply { + text = "+" + setTextSize(TypedValue.COMPLEX_UNIT_SP, 40f) + setTextColor(Color.WHITE) + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + ViewUtil.dpToPx(context, 20).let { setMargins(it, 0, it, 0) } + } + }.also(header::addView) + } + + drawables.firstOrNull()?.let(::addIcon) + drawables.drop(1).forEach { addPlus(); addIcon(it) } + + return context.showSessionDialog { + view(view) + button(R.string.Permissions_continue) { onPositive.run() } + button(R.string.Permissions_not_now) { onNegative.run() } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/SettingsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/permissions/SettingsDialog.kt new file mode 100644 index 000000000..a4efd8d87 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/SettingsDialog.kt @@ -0,0 +1,21 @@ +package org.thoughtcrime.securesms.permissions + +import android.content.Context +import network.loki.messenger.R +import org.thoughtcrime.securesms.showSessionDialog + +class SettingsDialog { + companion object { + @JvmStatic + fun show(context: Context, message: String) { + context.showSessionDialog { + title(R.string.Permissions_permission_required) + text(message) + button(R.string.Permissions_continue, R.string.AccessibilityId_continue) { + context.startActivity(Permissions.getApplicationSettingsIntent(context)) + } + cancelButton() + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt index 504194d3a..16499cc4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt @@ -1,67 +1,29 @@ package org.thoughtcrime.securesms.preferences -import android.app.AlertDialog import android.os.Bundle -import android.view.View import androidx.activity.viewModels import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ActivityBlockedContactsBinding import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.showSessionDialog @AndroidEntryPoint -class BlockedContactsActivity: PassphraseRequiredActionBarActivity(), View.OnClickListener { +class BlockedContactsActivity: PassphraseRequiredActionBarActivity() { lateinit var binding: ActivityBlockedContactsBinding val viewModel: BlockedContactsViewModel by viewModels() - val adapter = BlockedContactsAdapter() + val adapter: BlockedContactsAdapter by lazy { BlockedContactsAdapter(viewModel) } - override fun onClick(v: View?) { - if (v === binding.unblockButton && adapter.getSelectedItems().isNotEmpty()) { - val contactsToUnblock = adapter.getSelectedItems() - // show dialog - val title = if (contactsToUnblock.size == 1) { - getString(R.string.Unblock_dialog__title_single, contactsToUnblock.first().name) - } else { - getString(R.string.Unblock_dialog__title_multiple) - } - - val message = if (contactsToUnblock.size == 1) { - getString(R.string.Unblock_dialog__message, contactsToUnblock.first().name) - } else { - val stringBuilder = StringBuilder() - val iterator = contactsToUnblock.iterator() - var numberAdded = 0 - while (iterator.hasNext() && numberAdded < 3) { - val nextRecipient = iterator.next() - if (numberAdded > 0) stringBuilder.append(", ") - - stringBuilder.append(nextRecipient.name) - numberAdded++ - } - val overflow = contactsToUnblock.size - numberAdded - if (overflow > 0) { - stringBuilder.append(" ") - val string = resources.getQuantityString(R.plurals.Unblock_dialog__message_multiple_overflow, overflow) - stringBuilder.append(string.format(overflow)) - } - getString(R.string.Unblock_dialog__message, stringBuilder.toString()) - } - - AlertDialog.Builder(this) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.continue_2) { d, _ -> - viewModel.unblock(contactsToUnblock) - d.dismiss() - } - .setNegativeButton(R.string.cancel) { d, _ -> - d.dismiss() - } - .show() + fun unblock() { + showSessionDialog { + title(viewModel.getTitle(this@BlockedContactsActivity)) + text(viewModel.getMessage(this@BlockedContactsActivity)) + button(R.string.continue_2) { viewModel.unblock() } + cancelButton() } } @@ -73,15 +35,14 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity(), View.OnCli binding.recyclerView.adapter = adapter viewModel.subscribe(this) - .observe(this) { newState -> - adapter.submitList(newState.blockedContacts) - val isEmpty = newState.blockedContacts.isEmpty() - binding.emptyStateMessageTextView.isVisible = isEmpty - binding.nonEmptyStateGroup.isVisible = !isEmpty + .observe(this) { state -> + adapter.submitList(state.items) + binding.emptyStateMessageTextView.isVisible = state.emptyStateMessageTextViewVisible + binding.nonEmptyStateGroup.isVisible = state.nonEmptyStateGroupVisible + binding.unblockButton.isEnabled = state.unblockButtonEnabled } - binding.unblockButton.setOnClickListener(this) + binding.unblockButton.setOnClickListener { unblock() } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index 50af49b55..e0b92bdbe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -10,43 +10,35 @@ import network.loki.messenger.R import network.loki.messenger.databinding.BlockedContactLayoutBinding import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.util.adapter.SelectableItem -class BlockedContactsAdapter: ListAdapter(RecipientDiffer()) { +typealias SelectableRecipient = SelectableItem - class RecipientDiffer: DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem === newItem - override fun areContentsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem == newItem +class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdapter(RecipientDiffer()) { + + class RecipientDiffer: DiffUtil.ItemCallback() { + override fun areItemsTheSame(old: SelectableRecipient, new: SelectableRecipient) = old.item.address == new.item.address + override fun areContentsTheSame(old: SelectableRecipient, new: SelectableRecipient) = old.isSelected == new.isSelected + override fun getChangePayload(old: SelectableRecipient, new: SelectableRecipient) = new.isSelected } - private val selectedItems = mutableListOf() - - fun getSelectedItems() = selectedItems - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val itemView = LayoutInflater.from(parent.context).inflate(R.layout.blocked_contact_layout, parent, false) - return ViewHolder(itemView) - } - - private fun toggleSelection(recipient: Recipient, isSelected: Boolean, position: Int) { - if (isSelected) { - selectedItems -= recipient - } else { - selectedItems += recipient - } - notifyItemChanged(position) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + LayoutInflater.from(parent.context) + .inflate(R.layout.blocked_contact_layout, parent, false) + .let(::ViewHolder) override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val recipient = getItem(position) - val isSelected = recipient in selectedItems - holder.bind(recipient, isSelected) { - toggleSelection(recipient, isSelected, position) - } + holder.bind(getItem(position), viewModel::toggle) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + if (payloads.isEmpty()) holder.bind(getItem(position), viewModel::toggle) + else holder.select(getItem(position).isSelected) } override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) - holder.binding.profilePictureView.root.recycle() + holder.binding.profilePictureView.recycle() } class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { @@ -54,15 +46,17 @@ class BlockedContactsAdapter: ListAdapter Unit) { - binding.recipientName.text = recipient.name - with (binding.profilePictureView.root) { - glide = this@ViewHolder.glide - update(recipient) + fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) { + binding.recipientName.text = selectable.item.name + with (binding.profilePictureView) { + update(selectable.item) } - binding.root.setOnClickListener { toggleSelection() } + binding.root.setOnClickListener { toggle(selectable) } + binding.selectButton.isSelected = selectable.isSelected + } + + fun select(isSelected: Boolean) { binding.selectButton.isSelected = isSelected } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsLayout.kt deleted file mode 100644 index ed2970fbc..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsLayout.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.thoughtcrime.securesms.preferences - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout - -class BlockedContactsLayout @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null -) : FrameLayout(context, attrs) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsPreference.kt index 254d34978..48c7cc6dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsPreference.kt @@ -3,26 +3,19 @@ package org.thoughtcrime.securesms.preferences import android.content.Context import android.content.Intent import android.util.AttributeSet -import android.view.View import androidx.preference.PreferenceCategory import androidx.preference.PreferenceViewHolder class BlockedContactsPreference @JvmOverloads constructor( context: Context, - attributeSet: AttributeSet? = null) : PreferenceCategory(context, attributeSet), View.OnClickListener { - - override fun onClick(v: View?) { - if (v is BlockedContactsLayout) { - val intent = Intent(context, BlockedContactsActivity::class.java) - context.startActivity(intent) - } - } + attributeSet: AttributeSet? = null +) : PreferenceCategory(context, attributeSet) { override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) - val itemView = holder.itemView - itemView.setOnClickListener(this) + holder.itemView.setOnClickListener { + Intent(context, BlockedContactsActivity::class.java).let(context::startActivity) + } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt index 88819bcd9..85d97d8b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt @@ -7,8 +7,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.cash.copper.flow.observeQuery import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collect @@ -17,9 +19,13 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.plus import kotlinx.coroutines.withContext +import network.loki.messenger.R import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.DatabaseContentProviders +import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.adapter.SelectableItem import javax.inject.Inject @HiltViewModel @@ -29,7 +35,9 @@ class BlockedContactsViewModel @Inject constructor(private val storage: StorageP private val listUpdateChannel = Channel(capacity = Channel.CONFLATED) - private val _contacts = MutableLiveData(BlockedContactsViewState(emptyList())) + private val _state = MutableLiveData(BlockedContactsViewState()) + + val state get() = _state.value!! fun subscribe(context: Context): LiveData { executor.launch(IO) { @@ -45,21 +53,74 @@ class BlockedContactsViewModel @Inject constructor(private val storage: StorageP } executor.launch(IO) { for (update in listUpdateChannel) { - val blockedContactState = BlockedContactsViewState(storage.blockedContacts().sortedBy { it.name }) + val blockedContactState = state.copy( + blockedContacts = storage.blockedContacts().sortedBy { it.name } + ) withContext(Main) { - _contacts.value = blockedContactState + _state.value = blockedContactState } } } - return _contacts + return _state } - fun unblock(toUnblock: List) { - storage.unblock(toUnblock) + fun unblock() { + storage.setBlocked(state.selectedItems, false) + _state.value = state.copy(selectedItems = emptySet()) + } + + fun select(selectedItem: Recipient, isSelected: Boolean) { + _state.value = state.run { + if (isSelected) copy(selectedItems = selectedItems + selectedItem) + else copy(selectedItems = selectedItems - selectedItem) + } + } + + fun getTitle(context: Context): String = + if (state.selectedItems.size == 1) { + context.getString(R.string.Unblock_dialog__title_single, state.selectedItems.first().name) + } else { + context.getString(R.string.Unblock_dialog__title_multiple) + } + + fun getMessage(context: Context): String { + if (state.selectedItems.size == 1) { + return context.getString(R.string.Unblock_dialog__message, state.selectedItems.first().name) + } + val stringBuilder = StringBuilder() + val iterator = state.selectedItems.iterator() + var numberAdded = 0 + while (iterator.hasNext() && numberAdded < 3) { + val nextRecipient = iterator.next() + if (numberAdded > 0) stringBuilder.append(", ") + + stringBuilder.append(nextRecipient.name) + numberAdded++ + } + val overflow = state.selectedItems.size - numberAdded + if (overflow > 0) { + stringBuilder.append(" ") + val string = context.resources.getQuantityString(R.plurals.Unblock_dialog__message_multiple_overflow, overflow) + stringBuilder.append(string.format(overflow)) + } + return context.getString(R.string.Unblock_dialog__message, stringBuilder.toString()) + } + + fun toggle(selectable: SelectableItem) { + _state.value = state.run { + if (selectable.item in selectedItems) copy(selectedItems = selectedItems - selectable.item) + else copy(selectedItems = selectedItems + selectable.item) + } } data class BlockedContactsViewState( - val blockedContacts: List - ) + val blockedContacts: List = emptyList(), + val selectedItems: Set = emptySet() + ) { + val items = blockedContacts.map { SelectableItem(it, it in selectedItems) } -} \ No newline at end of file + val unblockButtonEnabled get() = selectedItems.isNotEmpty() + val emptyStateMessageTextViewVisible get() = blockedContacts.isEmpty() + val nonEmptyStateGroupVisible get() = blockedContacts.isNotEmpty() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt new file mode 100644 index 000000000..ea747798c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt @@ -0,0 +1,45 @@ +package org.thoughtcrime.securesms.preferences + +import android.Manifest +import androidx.fragment.app.Fragment +import androidx.preference.Preference +import network.loki.messenger.R +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences.Companion.setBooleanPreference +import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.showSessionDialog + +internal class CallToggleListener( + private val context: Fragment, + private val setCallback: (Boolean) -> Unit +) : Preference.OnPreferenceChangeListener { + + override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean { + if (newValue == false) return true + + // check if we've shown the info dialog and check for microphone permissions + context.showSessionDialog { + title(R.string.dialog_voice_video_title) + text(R.string.dialog_voice_video_message) + button(R.string.dialog_link_preview_enable_button_title, R.string.AccessibilityId_enable) { requestMicrophonePermission() } + cancelButton() + } + + return false + } + + private fun requestMicrophonePermission() { + Permissions.with(context) + .request(Manifest.permission.RECORD_AUDIO) + .onAllGranted { + setBooleanPreference( + context.requireContext(), + TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED, + true + ) + setCallback(true) + } + .onAnyDenied { setCallback(false) } + .execute() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt deleted file mode 100644 index 3d5b9e2e9..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.thoughtcrime.securesms.preferences - -import android.app.Dialog -import android.os.Bundle -import androidx.fragment.app.DialogFragment - -class ChangeUiModeDialog : DialogFragment() { - - companion object { - const val TAG = "ChangeUiModeDialog" - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val context = requireContext() - return android.app.AlertDialog.Builder(context) - .setTitle("TODO: remove this") - .show() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt index 560c13710..37a54a4af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt @@ -1,9 +1,12 @@ package org.thoughtcrime.securesms.preferences +import android.app.Dialog +import android.os.Bundle import android.view.LayoutInflater -import androidx.appcompat.app.AlertDialog +import android.view.View import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import kotlinx.coroutines.Dispatchers @@ -15,10 +18,10 @@ import network.loki.messenger.databinding.DialogClearAllDataBinding import org.session.libsession.snode.SnodeAPI import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -class ClearAllDataDialog : BaseDialog() { +class ClearAllDataDialog : DialogFragment() { private lateinit var binding: DialogClearAllDataBinding enum class Steps { @@ -35,13 +38,18 @@ class ClearAllDataDialog : BaseDialog() { updateUI() } - override fun setContentView(builder: AlertDialog.Builder) { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { + view(createView()) + } + + private fun createView(): View { binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext())) val device = RadioOption("deviceOnly", requireContext().getString(R.string.dialog_clear_all_data_clear_device_only)) val network = RadioOption("deviceAndNetwork", requireContext().getString(R.string.dialog_clear_all_data_clear_device_and_network)) var selectedOption = device val optionAdapter = RadioOptionAdapter { selectedOption = it } binding.recyclerView.apply { + itemAnimator = null adapter = optionAdapter addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) setHasFixedSize(true) @@ -61,8 +69,7 @@ class ClearAllDataDialog : BaseDialog() { Steps.DELETING -> { /* do nothing intentionally */ } } } - builder.setView(binding.root) - builder.setCancelable(false) + return binding.root } private fun updateUI() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java index badcbe66b..8c3e6190e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java @@ -24,8 +24,6 @@ import androidx.recyclerview.widget.RecyclerView; import org.thoughtcrime.securesms.components.CustomDefaultPreference; import org.thoughtcrime.securesms.conversation.v2.ViewUtil; -import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreference; -import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreferenceDialogFragmentCompat; import network.loki.messenger.R; @@ -60,9 +58,7 @@ public abstract class CorrectedPreferenceFragment extends PreferenceFragmentComp public void onDisplayPreferenceDialog(Preference preference) { DialogFragment dialogFragment = null; - if (preference instanceof ColorPickerPreference) { - dialogFragment = ColorPickerPreferenceDialogFragmentCompat.newInstance(preference.getKey()); - } else if (preference instanceof CustomDefaultPreference) { + if (preference instanceof CustomDefaultPreference) { dialogFragment = CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.newInstance(preference.getKey()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ListPreferenceDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ListPreferenceDialog.kt index e407c6777..6f0998eec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ListPreferenceDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ListPreferenceDialog.kt @@ -1,42 +1,24 @@ package org.thoughtcrime.securesms.preferences -import android.view.LayoutInflater +import android.content.Context import androidx.appcompat.app.AlertDialog import androidx.preference.ListPreference -import androidx.recyclerview.widget.DividerItemDecoration -import network.loki.messenger.databinding.DialogListPreferenceBinding -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.showSessionDialog -class ListPreferenceDialog( - private val listPreference: ListPreference, - private val dialogListener: () -> Unit -) : BaseDialog() { - private lateinit var binding: DialogListPreferenceBinding +fun listPreferenceDialog( + context: Context, + listPreference: ListPreference, + onChange: () -> Unit +) : AlertDialog = listPreference.run { + context.showSessionDialog { + val index = entryValues.indexOf(value) + val options = entries.map(CharSequence::toString).toTypedArray() - override fun setContentView(builder: AlertDialog.Builder) { - binding = DialogListPreferenceBinding.inflate(LayoutInflater.from(requireContext())) - binding.titleTextView.text = listPreference.dialogTitle - binding.messageTextView.text = listPreference.dialogMessage - binding.closeButton.setOnClickListener { - dismiss() + title(dialogTitle) + text(dialogMessage) + singleChoiceItems(options, index) { + listPreference.setValueIndex(it) + onChange() } - val options = listPreference.entryValues.zip(listPreference.entries) { value, title -> - RadioOption(value.toString(), title.toString()) - } - val valueIndex = listPreference.findIndexOfValue(listPreference.value) - val optionAdapter = RadioOptionAdapter(valueIndex) { - listPreference.value = it.value - dismiss() - dialogListener.invoke() - } - binding.recyclerView.apply { - adapter = optionAdapter - addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) - setHasFixedSize(true) - } - optionAdapter.submitList(options) - builder.setView(binding.root) - builder.setCancelable(false) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt index af039a4fd..b18859ea0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt @@ -1,10 +1,12 @@ package org.thoughtcrime.securesms.preferences import android.os.Bundle +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment +@AndroidEntryPoint class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java deleted file mode 100644 index 9ae78fc5c..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ /dev/null @@ -1,180 +0,0 @@ -package org.thoughtcrime.securesms.preferences; - -import static android.app.Activity.RESULT_OK; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.Settings; -import android.text.TextUtils; - -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; -import androidx.preference.Preference; - -import org.session.libsession.utilities.TextSecurePreferences; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; -import org.thoughtcrime.securesms.notifications.NotificationChannels; - -import network.loki.messenger.R; - -public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment { - - @SuppressWarnings("unused") - private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName(); - - @Override - public void onCreate(Bundle paramBundle) { - super.onCreate(paramBundle); - - // Set up FCM toggle - String fcmKey = "pref_key_use_fcm"; - ((SwitchPreferenceCompat)findPreference(fcmKey)).setChecked(TextSecurePreferences.isUsingFCM(getContext())); - this.findPreference(fcmKey) - .setOnPreferenceChangeListener((preference, newValue) -> { - TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue); - ApplicationContext.getInstance(getContext()).registerForFCMIfNeeded(true); - return true; - }); - - if (NotificationChannels.supported()) { - TextSecurePreferences.setNotificationRingtone(getContext(), NotificationChannels.getMessageRingtone(getContext()).toString()); - TextSecurePreferences.setNotificationVibrateEnabled(getContext(), NotificationChannels.getMessageVibrate(getContext())); - } - this.findPreference(TextSecurePreferences.RINGTONE_PREF) - .setOnPreferenceChangeListener(new RingtoneSummaryListener()); - this.findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) - .setOnPreferenceChangeListener(new NotificationPrivacyListener()); - this.findPreference(TextSecurePreferences.VIBRATE_PREF) - .setOnPreferenceChangeListener((preference, newValue) -> { - NotificationChannels.updateMessageVibrate(getContext(), (boolean) newValue); - return true; - }); - - this.findPreference(TextSecurePreferences.RINGTONE_PREF) - .setOnPreferenceClickListener(preference -> { - Uri current = TextSecurePreferences.getNotificationRingtone(getContext()); - - Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current); - - startActivityForResult(intent, 1); - - return true; - }); - - this.findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) - .setOnPreferenceClickListener(preference -> { - ListPreference listPreference = (ListPreference) preference; - listPreference.setDialogMessage(R.string.preferences_notifications__content_message); - new ListPreferenceDialog(listPreference, () -> { - initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)); - return null; - }).show(getChildFragmentManager(), "ListPreferenceDialog"); - return true; - }); - - initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)); - - if (NotificationChannels.supported()) { - this.findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF) - .setOnPreferenceClickListener(preference -> { - Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(getContext())); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName()); - startActivity(intent); - return true; - }); - } - - initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)); - initializeMessageVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.VIBRATE_PREF)); - } - - @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_notifications); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == 1 && resultCode == RESULT_OK && data != null) { - Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); - - if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) { - NotificationChannels.updateMessageRingtone(getContext(), uri); - TextSecurePreferences.removeNotificationRingtone(getContext()); - } else { - uri = uri == null ? Uri.EMPTY : uri; - NotificationChannels.updateMessageRingtone(getContext(), uri); - TextSecurePreferences.setNotificationRingtone(getContext(), uri.toString()); - } - - initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)); - } - } - - private class RingtoneSummaryListener implements Preference.OnPreferenceChangeListener { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - Uri value = (Uri) newValue; - - if (value == null || TextUtils.isEmpty(value.toString())) { - preference.setSummary(R.string.preferences__silent); - } else { - Ringtone tone = RingtoneManager.getRingtone(getActivity(), value); - - if (tone != null) { - preference.setSummary(tone.getTitle(getActivity())); - } - } - - return true; - } - } - - private void initializeRingtoneSummary(Preference pref) { - RingtoneSummaryListener listener = (RingtoneSummaryListener) pref.getOnPreferenceChangeListener(); - Uri uri = TextSecurePreferences.getNotificationRingtone(getContext()); - - listener.onPreferenceChange(pref, uri); - } - - private void initializeMessageVibrateSummary(SwitchPreferenceCompat pref) { - pref.setChecked(TextSecurePreferences.isNotificationVibrateEnabled(getContext())); - } - - public static CharSequence getSummary(Context context) { - final int onCapsResId = R.string.ApplicationPreferencesActivity_On; - final int offCapsResId = R.string.ApplicationPreferencesActivity_Off; - - return context.getString(TextSecurePreferences.isNotificationsEnabled(context) ? onCapsResId : offCapsResId); - } - - private class NotificationPrivacyListener extends ListSummaryListener { - @SuppressLint("StaticFieldLeak") - @Override - public boolean onPreferenceChange(Preference preference, Object value) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - ApplicationContext.getInstance(getActivity()).messageNotifier.updateNotification(getActivity()); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - - return super.onPreferenceChange(preference, value); - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt new file mode 100644 index 000000000..fa6461acc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt @@ -0,0 +1,183 @@ +package org.thoughtcrime.securesms.preferences + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +import android.os.AsyncTask +import android.os.Bundle +import android.provider.Settings +import android.text.TextUtils +import androidx.lifecycle.lifecycleScope +import androidx.preference.ListPreference +import androidx.preference.Preference +import dagger.hilt.android.AndroidEntryPoint +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.libsession.utilities.TextSecurePreferences.Companion.isNotificationsEnabled +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.components.SwitchPreferenceCompat +import org.thoughtcrime.securesms.notifications.NotificationChannels +import org.thoughtcrime.securesms.notifications.PushRegistry +import javax.inject.Inject + +@AndroidEntryPoint +class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() { + @Inject + lateinit var pushRegistry: PushRegistry + @Inject + lateinit var prefs: TextSecurePreferences + + override fun onCreate(paramBundle: Bundle?) { + super.onCreate(paramBundle) + + // Set up FCM toggle + val fcmKey = "pref_key_use_fcm" + val fcmPreference: SwitchPreferenceCompat = findPreference(fcmKey)!! + fcmPreference.isChecked = prefs.isPushEnabled() + fcmPreference.setOnPreferenceChangeListener { _: Preference, newValue: Any -> + prefs.setPushEnabled(newValue as Boolean) + val job = pushRegistry.refresh(true) + + fcmPreference.isEnabled = false + + lifecycleScope.launch(Dispatchers.IO) { + job.join() + + withContext(Dispatchers.Main) { + fcmPreference.isEnabled = true + } + } + + true + } + if (NotificationChannels.supported()) { + prefs.setNotificationRingtone( + NotificationChannels.getMessageRingtone(requireContext()).toString() + ) + prefs.setNotificationVibrateEnabled( + NotificationChannels.getMessageVibrate(requireContext()) + ) + } + findPreference(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceChangeListener = RingtoneSummaryListener() + findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceChangeListener = NotificationPrivacyListener() + findPreference(TextSecurePreferences.VIBRATE_PREF)!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + NotificationChannels.updateMessageVibrate(requireContext(), newValue as Boolean) + true + } + findPreference(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val current = prefs.getNotificationRingtone() + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) + intent.putExtra( + RingtoneManager.EXTRA_RINGTONE_TYPE, + RingtoneManager.TYPE_NOTIFICATION + ) + intent.putExtra( + RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + Settings.System.DEFAULT_NOTIFICATION_URI + ) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current) + startActivityForResult(intent, 1) + true + } + findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { preference: Preference -> + val listPreference = preference as ListPreference + listPreference.setDialogMessage(R.string.preferences_notifications__content_message) + listPreferenceDialog(requireContext(), listPreference) { + initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)) + } + true + } + initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) as ListPreference?) + if (NotificationChannels.supported()) { + findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + intent.putExtra( + Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(requireContext()) + ) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) + startActivity(intent) + true + } + } + initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)) + initializeMessageVibrateSummary(findPreference(TextSecurePreferences.VIBRATE_PREF) as SwitchPreferenceCompat?) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_notifications) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == 1 && resultCode == Activity.RESULT_OK && data != null) { + var uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + if (Settings.System.DEFAULT_NOTIFICATION_URI == uri) { + NotificationChannels.updateMessageRingtone(requireContext(), uri) + prefs.removeNotificationRingtone() + } else { + uri = uri ?: Uri.EMPTY + NotificationChannels.updateMessageRingtone(requireContext(), uri) + prefs.setNotificationRingtone(uri.toString()) + } + initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)) + } + } + + private inner class RingtoneSummaryListener : Preference.OnPreferenceChangeListener { + override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean { + val value = newValue as? Uri + if (value == null || TextUtils.isEmpty(value.toString())) { + preference.setSummary(R.string.preferences__silent) + } else { + RingtoneManager.getRingtone(activity, value) + ?.getTitle(activity) + ?.let { preference.summary = it } + + } + return true + } + } + + private fun initializeRingtoneSummary(pref: Preference?) { + val listener = pref!!.onPreferenceChangeListener as RingtoneSummaryListener? + val uri = prefs.getNotificationRingtone() + listener!!.onPreferenceChange(pref, uri) + } + + private fun initializeMessageVibrateSummary(pref: SwitchPreferenceCompat?) { + pref!!.isChecked = prefs.isNotificationVibrateEnabled() + } + + private inner class NotificationPrivacyListener : ListSummaryListener() { + @SuppressLint("StaticFieldLeak") + override fun onPreferenceChange(preference: Preference, value: Any): Boolean { + object : AsyncTask() { + override fun doInBackground(vararg params: Void?): Void? { + ApplicationContext.getInstance(activity).messageNotifier.updateNotification(activity!!) + return null + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + return super.onPreferenceChange(preference, value) + } + } + + companion object { + @Suppress("unused") + private val TAG = NotificationsPreferenceFragment::class.java.simpleName + fun getSummary(context: Context): CharSequence = when (isNotificationsEnabled(context)) { + true -> R.string.ApplicationPreferencesActivity_On + false -> R.string.ApplicationPreferencesActivity_Off + }.let(context::getString) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.java deleted file mode 100644 index b5774447e..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.java +++ /dev/null @@ -1,199 +0,0 @@ -package org.thoughtcrime.securesms.preferences; - -import android.Manifest; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.KeyguardManager; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.provider.Settings; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.fragment.app.Fragment; -import androidx.preference.Preference; - -import org.session.libsession.utilities.TextSecurePreferences; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; -import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.util.CallNotificationBuilder; -import org.thoughtcrime.securesms.util.IntentUtils; - -import kotlin.jvm.functions.Function1; -import network.loki.messenger.BuildConfig; -import network.loki.messenger.R; - -public class PrivacySettingsPreferenceFragment extends ListSummaryPreferenceFragment { - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - } - - @Override - public void onCreate(Bundle paramBundle) { - super.onCreate(paramBundle); - - this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener()); - - this.findPreference(TextSecurePreferences.READ_RECEIPTS_PREF).setOnPreferenceChangeListener(new ReadReceiptToggleListener()); - this.findPreference(TextSecurePreferences.TYPING_INDICATORS).setOnPreferenceChangeListener(new TypingIndicatorsToggleListener()); - this.findPreference(TextSecurePreferences.LINK_PREVIEWS).setOnPreferenceChangeListener(new LinkPreviewToggleListener()); - this.findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED).setOnPreferenceChangeListener(new CallToggleListener(this, this::setCall)); - - initializeVisibility(); - } - - private Void setCall(boolean isEnabled) { - ((SwitchPreferenceCompat)findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)).setChecked(isEnabled); - if (isEnabled && !CallNotificationBuilder.areNotificationsEnabled(requireActivity())) { - // show a dialog saying that calls won't work properly if you don't have notifications on at a system level - new AlertDialog.Builder(new ContextThemeWrapper(requireActivity(), R.style.ThemeOverlay_Session_AlertDialog)) - .setTitle(R.string.CallNotificationBuilder_system_notification_title) - .setMessage(R.string.CallNotificationBuilder_system_notification_message) - .setPositiveButton(R.string.activity_notification_settings_title, (d, w) -> { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - Intent settingsIntent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID); - if (IntentUtils.isResolvable(requireContext(), settingsIntent)) { - startActivity(settingsIntent); - } - } else { - Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setData(Uri.parse("package:"+BuildConfig.APPLICATION_ID)); - if (IntentUtils.isResolvable(requireContext(), settingsIntent)) { - startActivity(settingsIntent); - } - } - d.dismiss(); - }) - .setNeutralButton(R.string.dismiss, (d, w) -> { - // do nothing, user might have broken notifications - d.dismiss(); - }) - .show(); - } - return null; - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); - } - - @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_app_protection); - } - - @Override - public void onResume() { - super.onResume(); - } - - private void initializeVisibility() { - if (TextSecurePreferences.isPasswordDisabled(getContext())) { - KeyguardManager keyguardManager = (KeyguardManager)getContext().getSystemService(Context.KEYGUARD_SERVICE); - if (!keyguardManager.isKeyguardSecure()) { - ((SwitchPreferenceCompat)findPreference(TextSecurePreferences.SCREEN_LOCK)).setChecked(false); - findPreference(TextSecurePreferences.SCREEN_LOCK).setEnabled(false); - } - } else { - findPreference(TextSecurePreferences.SCREEN_LOCK).setVisible(false); - findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setVisible(false); - } - } - - private class ScreenLockListener implements Preference.OnPreferenceChangeListener { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = (Boolean)newValue; - - TextSecurePreferences.setScreenLockEnabled(getContext(), enabled); - - Intent intent = new Intent(getContext(), KeyCachingService.class); - intent.setAction(KeyCachingService.LOCK_TOGGLED_EVENT); - getContext().startService(intent); - return true; - } - } - - private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - return true; - } - } - - private class TypingIndicatorsToggleListener implements Preference.OnPreferenceChangeListener { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = (boolean)newValue; - - if (!enabled) { - ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().clear(); - } - - return true; - } - } - - private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - return true; - } - } - - private class CallToggleListener implements Preference.OnPreferenceChangeListener { - - private final Fragment context; - private final Function1 setCallback; - - private CallToggleListener(Fragment context, Function1 setCallback) { - this.context = context; - this.setCallback = setCallback; - } - - private void requestMicrophonePermission() { - Permissions.with(context) - .request(Manifest.permission.RECORD_AUDIO) - .onAllGranted(() -> { - TextSecurePreferences.setBooleanPreference(context.requireContext(), TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED, true); - setCallback.invoke(true); - }) - .onAnyDenied(() -> setCallback.invoke(false)) - .execute(); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean val = (boolean) newValue; - if (val) { - // check if we've shown the info dialog and check for microphone permissions - new AlertDialog.Builder(new ContextThemeWrapper(context.requireContext(), R.style.ThemeOverlay_Session_AlertDialog)) - .setTitle(R.string.dialog_voice_video_title) - .setMessage(R.string.dialog_voice_video_message) - .setPositiveButton(R.string.dialog_link_preview_enable_button_title, (d, w) -> { - requestMicrophonePermission(); - }) - .setNegativeButton(R.string.cancel, (d, w) -> { - - }) - .show(); - return false; - } else { - return true; - } - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt new file mode 100644 index 000000000..eaf48f868 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt @@ -0,0 +1,114 @@ +package org.thoughtcrime.securesms.preferences + +import android.app.KeyguardManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import androidx.preference.Preference +import network.loki.messenger.BuildConfig +import network.loki.messenger.R +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences.Companion.isPasswordDisabled +import org.session.libsession.utilities.TextSecurePreferences.Companion.setScreenLockEnabled +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.components.SwitchPreferenceCompat +import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.service.KeyCachingService +import org.thoughtcrime.securesms.showSessionDialog +import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNotificationsEnabled +import org.thoughtcrime.securesms.util.IntentUtils + +class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() { + override fun onCreate(paramBundle: Bundle?) { + super.onCreate(paramBundle) + findPreference(TextSecurePreferences.SCREEN_LOCK)!! + .onPreferenceChangeListener = ScreenLockListener() + findPreference(TextSecurePreferences.TYPING_INDICATORS)!! + .onPreferenceChangeListener = TypingIndicatorsToggleListener() + findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!! + .onPreferenceChangeListener = CallToggleListener(this) { setCall(it) } + initializeVisibility() + } + + private fun setCall(isEnabled: Boolean) { + (findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED) as SwitchPreferenceCompat?)!!.isChecked = + isEnabled + if (isEnabled && !areNotificationsEnabled(requireActivity())) { + // show a dialog saying that calls won't work properly if you don't have notifications on at a system level + showSessionDialog { + title(R.string.CallNotificationBuilder_system_notification_title) + text(R.string.CallNotificationBuilder_system_notification_message) + button(R.string.activity_notification_settings_title) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID) + } else { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)) + } + .apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } + .takeIf { IntentUtils.isResolvable(requireContext(), it) }.let { + startActivity(it) + } + } + button(R.string.dismiss) + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_app_protection) + } + + override fun onResume() { + super.onResume() + } + + private fun initializeVisibility() { + if (isPasswordDisabled(requireContext())) { + val keyguardManager = + requireContext().getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + if (!keyguardManager.isKeyguardSecure) { + findPreference(TextSecurePreferences.SCREEN_LOCK)!!.isChecked = false + findPreference(TextSecurePreferences.SCREEN_LOCK)!!.isEnabled = false + } + } else { + findPreference(TextSecurePreferences.SCREEN_LOCK)!!.isVisible = false + findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT)!!.isVisible = false + } + } + + private inner class ScreenLockListener : Preference.OnPreferenceChangeListener { + override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean { + val enabled = newValue as Boolean + setScreenLockEnabled(context!!, enabled) + val intent = Intent(context, KeyCachingService::class.java) + intent.action = KeyCachingService.LOCK_TOGGLED_EVENT + context!!.startService(intent) + return true + } + } + + private inner class TypingIndicatorsToggleListener : Preference.OnPreferenceChangeListener { + override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean { + val enabled = newValue as Boolean + if (!enabled) { + ApplicationContext.getInstance(requireContext()).typingStatusRepository.clear() + } + return true + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt index 2cb61a0e8..4bb69c4c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt @@ -16,8 +16,8 @@ class RadioOptionAdapter( ) : ListAdapter(RadioOptionDiffer()) { class RadioOptionDiffer: DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem === newItem - override fun areContentsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem == newItem + override fun areItemsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem.title == newItem.title + override fun areContentsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem.value == newItem.value } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -31,7 +31,7 @@ class RadioOptionAdapter( holder.bind(option, isSelected) { onClickListener(it) selectedOptionPosition = position - notifyDataSetChanged() + notifyItemRangeChanged(0, itemCount) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt index e7bfd60d3..bae5f1960 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt @@ -1,38 +1,34 @@ package org.thoughtcrime.securesms.preferences +import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.view.LayoutInflater +import android.os.Bundle import android.widget.Toast -import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment import network.loki.messenger.R -import network.loki.messenger.databinding.DialogSeedBinding import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.hexEncodedPrivateKey +import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog - -class SeedDialog : BaseDialog() { +class SeedDialog: DialogFragment() { private val seed by lazy { - var hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED) - if (hexEncodedSeed == null) { - hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(requireContext()).hexEncodedPrivateKey // Legacy account - } - val loadFileContents: (String) -> String = { fileName -> - MnemonicUtilities.loadFileContents(requireContext(), fileName) - } - MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english) + val hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED) + ?: IdentityKeyUtil.getIdentityKeyPair(requireContext()).hexEncodedPrivateKey // Legacy account + + MnemonicCodec { fileName -> MnemonicUtilities.loadFileContents(requireContext(), fileName) } + .encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english) } - override fun setContentView(builder: AlertDialog.Builder) { - val binding = DialogSeedBinding.inflate(LayoutInflater.from(requireContext())) - binding.seedTextView.text = seed - binding.closeButton.setOnClickListener { dismiss() } - binding.copyButton.setOnClickListener { copySeed() } - builder.setView(binding.root) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { + title(R.string.dialog_seed_title) + text(R.string.dialog_seed_explanation) + text(seed, R.style.SessionIDTextView) + button(R.string.copy, R.string.AccessibilityId_copy_recovery_phrase) { copySeed() } + button(R.string.close) { dismiss() } } private fun copySeed() { @@ -42,4 +38,4 @@ class SeedDialog : BaseDialog() { Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() dismiss() } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 0a56c5058..5f2485576 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -20,21 +20,26 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.core.view.isVisible +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.BuildConfig import network.loki.messenger.R import network.loki.messenger.databinding.ActivitySettingsBinding +import network.loki.messenger.libsession_util.util.UserPic import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.successUi import org.session.libsession.avatars.AvatarHelper -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.ProfileKeyUtil -import org.session.libsession.utilities.ProfilePictureUtilities +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.avatars.ProfileContactPhoto +import org.session.libsession.utilities.* import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol -import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection +import org.thoughtcrime.securesms.components.ProfilePictureView +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.home.PathActivity import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.mms.GlideApp @@ -42,6 +47,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints +import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities @@ -50,15 +56,18 @@ import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show import java.io.File import java.security.SecureRandom -import java.util.Date +import javax.inject.Inject +@AndroidEntryPoint class SettingsActivity : PassphraseRequiredActionBarActivity() { + + @Inject + lateinit var configFactory: ConfigFactory + private lateinit var binding: ActivitySettingsBinding private var displayNameEditActionMode: ActionMode? = null set(value) { field = value; handleDisplayNameEditActionModeChanged() } private lateinit var glide: GlideRequests - private var displayNameToBeUploaded: String? = null - private var profilePictureToBeUploaded: ByteArray? = null private var tempFile: File? = null private val hexEncodedPublicKey: String @@ -76,15 +85,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { super.onCreate(savedInstanceState, isReady) binding = ActivitySettingsBinding.inflate(layoutInflater) setContentView(binding.root) - val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey + val displayName = getDisplayName() glide = GlideApp.with(this) with(binding) { - profilePictureView.root.glide = glide - profilePictureView.root.publicKey = hexEncodedPublicKey - profilePictureView.root.displayName = displayName - profilePictureView.root.isLarge = true - profilePictureView.root.update() - profilePictureView.root.setOnClickListener { showEditProfilePictureUI() } + setupProfilePictureView(profilePictureView) + profilePictureView.setOnClickListener { showEditProfilePictureUI() } ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } btnGroupNameDisplay.text = displayName publicKeyTextView.text = hexEncodedPublicKey @@ -105,6 +110,18 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } } + private fun getDisplayName(): String = + TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey) + + private fun setupProfilePictureView(view: ProfilePictureView) { + view.apply { + publicKey = hexEncodedPublicKey + displayName = getDisplayName() + isLarge = true + update() + } + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val scrollBundle = SparseArray() @@ -154,9 +171,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } AsyncTask.execute { try { - profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap + val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap Handler(Looper.getMainLooper()).post { - updateProfile(true) + updateProfile(true, profilePictureToBeUploaded) } } catch (e: BitmapDecodingException) { e.printStackTrace() @@ -190,40 +207,54 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } } - private fun updateProfile(isUpdatingProfilePicture: Boolean) { + private fun updateProfile( + isUpdatingProfilePicture: Boolean, + profilePicture: ByteArray? = null, + displayName: String? = null + ) { binding.loader.isVisible = true val promises = mutableListOf>() - val displayName = displayNameToBeUploaded if (displayName != null) { TextSecurePreferences.setProfileName(this, displayName) + configFactory.user?.setName(displayName) } - val profilePicture = profilePictureToBeUploaded val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this) - if (isUpdatingProfilePicture && profilePicture != null) { - promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)) + if (isUpdatingProfilePicture) { + if (profilePicture != null) { + promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)) + } else { + MessagingModuleConfiguration.shared.storage.clearUserPic() + } } val compoundPromise = all(promises) compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below - if (isUpdatingProfilePicture && profilePicture != null) { + val userConfig = configFactory.user + if (isUpdatingProfilePicture) { AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) - TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) - TextSecurePreferences.setLastProfilePictureUpload(this, Date().time) + TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 ) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) + // new config + val url = TextSecurePreferences.getProfilePictureURL(this) + val profileKey = ProfileKeyUtil.getProfileKey(this) + if (profilePicture == null) { + userConfig?.setPic(UserPic.DEFAULT) + } else if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) { + userConfig?.setPic(UserPic(url, profileKey)) + } } - if (profilePicture != null || displayName != null) { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) + if (userConfig != null && userConfig.needsDump()) { + configFactory.persist(userConfig, SnodeAPI.nowWithOffset) } + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) } compoundPromise.alwaysUi { if (displayName != null) { binding.btnGroupNameDisplay.text = displayName } - if (isUpdatingProfilePicture && profilePicture != null) { - binding.profilePictureView.root.recycle() // Clear the cached image before updating - binding.profilePictureView.root.update() + if (isUpdatingProfilePicture) { + binding.profilePictureView.recycle() // Clear the cached image before updating + binding.profilePictureView.update() } - displayNameToBeUploaded = null - profilePictureToBeUploaded = null binding.loader.isVisible = false } } @@ -244,8 +275,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show() return false } - displayNameToBeUploaded = displayName - updateProfile(false) + updateProfile(false, displayName = displayName) return true } @@ -255,6 +285,34 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } private fun showEditProfilePictureUI() { + showSessionDialog { + title(R.string.activity_settings_set_display_picture) + view(R.layout.dialog_change_avatar) + button(R.string.activity_settings_upload) { startAvatarSelection() } + if (TextSecurePreferences.getProfileAvatarId(context) != 0) { + button(R.string.activity_settings_remove) { removeAvatar() } + } + cancelButton() + }.apply { + val profilePic = findViewById(R.id.profile_picture_view) + ?.also(::setupProfilePictureView) + + val pictureIcon = findViewById(R.id.ic_pictures) + + val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false) + + val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "") + + profilePic?.isVisible = photoSet + pictureIcon?.isVisible = !photoSet + } + } + + private fun removeAvatar() { + updateProfile(true) + } + + private fun startAvatarSelection() { // Ask for an optional camera permission. Permissions.with(this) .request(Manifest.permission.CAMERA) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt index 1bd837324..2dc5e75d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt @@ -1,17 +1,18 @@ package org.thoughtcrime.securesms.preferences +import android.app.Dialog import android.content.ContentResolver import android.content.ContentValues import android.content.Intent import android.media.MediaScannerConnection import android.net.Uri import android.os.Build +import android.os.Bundle import android.os.Environment import android.provider.MediaStore -import android.view.LayoutInflater import android.webkit.MimeTypeMap import android.widget.Toast -import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Main @@ -20,11 +21,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.BuildConfig import network.loki.messenger.R -import network.loki.messenger.databinding.DialogShareLogsBinding import org.session.libsignal.utilities.ExternalStorageUtil import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.util.FileProviderUtil import org.thoughtcrime.securesms.util.StreamUtil import java.io.File @@ -33,21 +33,15 @@ import java.io.IOException import java.util.Objects import java.util.concurrent.TimeUnit -class ShareLogsDialog : BaseDialog() { +class ShareLogsDialog : DialogFragment() { private var shareJob: Job? = null - override fun setContentView(builder: AlertDialog.Builder) { - val binding = DialogShareLogsBinding.inflate(LayoutInflater.from(requireContext())) - binding.cancelButton.setOnClickListener { - dismiss() - } - binding.shareButton.setOnClickListener { - // start the export and share - shareLogs() - } - builder.setView(binding.root) - builder.setCancelable(false) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { + title(R.string.dialog_share_logs_title) + text(R.string.dialog_share_logs_explanation) + button(R.string.share, dismiss = false) { shareLogs() } + cancelButton { dismiss() } } private fun shareLogs() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsActivity.kt index bfeea554e..823728c35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsActivity.kt @@ -78,8 +78,6 @@ class AppearanceSettingsActivity: PassphraseRequiredActionBarActivity(), View.On viewModel.setNewAccent(R.style.PrimaryGreen) } } - } else if (v == binding.systemSettingsSwitch) { - viewModel.setNewFollowSystemSettings((v as SwitchCompat).isChecked) } } @@ -115,12 +113,8 @@ class AppearanceSettingsActivity: PassphraseRequiredActionBarActivity(), View.On super.onCreate(savedInstanceState, ready) binding = ActivityAppearanceSettingsBinding.inflate(layoutInflater) setContentView(binding.root) - savedInstanceState?.let { bundle -> - val scrollStateParcel = bundle.getSparseParcelableArray(SCROLL_PARCEL) - if (scrollStateParcel != null) { - binding.scrollView.restoreHierarchyState(scrollStateParcel) - } - } + savedInstanceState?.getSparseParcelableArray(SCROLL_PARCEL) + ?.let(binding.scrollView::restoreHierarchyState) supportActionBar!!.title = getString(R.string.activity_settings_message_appearance_button_title) with (binding) { // accent toggles @@ -132,7 +126,8 @@ class AppearanceSettingsActivity: PassphraseRequiredActionBarActivity(), View.On it.setOnClickListener(this@AppearanceSettingsActivity) } // system settings toggle - systemSettingsSwitch.setOnClickListener(this@AppearanceSettingsActivity) + systemSettingsSwitch.setOnCheckedChangeListener { _, isChecked -> viewModel.setNewFollowSystemSettings(isChecked) } + systemSettingsSwitchHolder.setOnClickListener { systemSettingsSwitch.toggle() } } lifecycleScope.launchWhenResumed { @@ -148,6 +143,5 @@ class AppearanceSettingsActivity: PassphraseRequiredActionBarActivity(), View.On } } } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreference.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreference.java deleted file mode 100644 index 1cccf1d52..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreference.java +++ /dev/null @@ -1,251 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import androidx.core.content.ContextCompat; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.DialogPreference; -import androidx.preference.PreferenceViewHolder; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.widget.ImageView; - -import com.takisoft.colorpicker.ColorPickerDialog; -import com.takisoft.colorpicker.ColorPickerDialog.Size; -import com.takisoft.colorpicker.ColorStateDrawable; - -import network.loki.messenger.R; - -public class ColorPickerPreference extends DialogPreference { - - private static final String TAG = ColorPickerPreference.class.getSimpleName(); - - private int[] colors; - private CharSequence[] colorDescriptions; - private int color; - private int columns; - private int size; - private boolean sortColors; - - private ImageView colorWidget; - private OnPreferenceChangeListener listener; - - public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference, defStyleAttr, 0); - - int colorsId = a.getResourceId(R.styleable.ColorPickerPreference_colors, R.array.color_picker_default_colors); - - if (colorsId != 0) { - colors = context.getResources().getIntArray(colorsId); - } - - colorDescriptions = a.getTextArray(R.styleable.ColorPickerPreference_colorDescriptions); - color = a.getColor(R.styleable.ColorPickerPreference_currentColor, 0); - columns = a.getInt(R.styleable.ColorPickerPreference_columns, 3); - size = a.getInt(R.styleable.ColorPickerPreference_colorSize, 2); - sortColors = a.getBoolean(R.styleable.ColorPickerPreference_sortColors, false); - - a.recycle(); - - setWidgetLayoutResource(R.layout.preference_widget_color_swatch); - } - - public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - @SuppressLint("RestrictedApi") - public ColorPickerPreference(Context context, AttributeSet attrs) { - this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.dialogPreferenceStyle, - android.R.attr.dialogPreferenceStyle)); - } - - public ColorPickerPreference(Context context) { - this(context, null); - } - - @Override - public void setOnPreferenceChangeListener(OnPreferenceChangeListener listener) { - super.setOnPreferenceChangeListener(listener); - this.listener = listener; - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - - colorWidget = (ImageView) holder.findViewById(R.id.color_picker_widget); - setColorOnWidget(color); - } - - private void setColorOnWidget(int color) { - if (colorWidget == null) { - return; - } - - Drawable[] colorDrawable = new Drawable[] - {ContextCompat.getDrawable(getContext(), R.drawable.colorpickerpreference_pref_swatch)}; - colorWidget.setImageDrawable(new ColorStateDrawable(colorDrawable, color)); - } - - /** - * Returns the current color. - * - * @return The current color. - */ - public int getColor() { - return color; - } - - /** - * Sets the current color. - * - * @param color The current color. - */ - public void setColor(int color) { - setInternalColor(color, false); - } - - /** - * Returns all of the available colors. - * - * @return The available colors. - */ - public int[] getColors() { - return colors; - } - - /** - * Sets the available colors. - * - * @param colors The available colors. - */ - public void setColors(int[] colors) { - this.colors = colors; - } - - /** - * Returns whether the available colors should be sorted automatically based on their HSV - * values. - * - * @return Whether the available colors should be sorted automatically based on their HSV - * values. - */ - public boolean isSortColors() { - return sortColors; - } - - /** - * Sets whether the available colors should be sorted automatically based on their HSV - * values. The sorting does not modify the order of the original colors supplied via - * {@link #setColors(int[])} or the XML attribute {@code app:colors}. - * - * @param sortColors Whether the available colors should be sorted automatically based on their - * HSV values. - */ - public void setSortColors(boolean sortColors) { - this.sortColors = sortColors; - } - - /** - * Returns the available colors' descriptions that can be used by accessibility services. - * - * @return The available colors' descriptions. - */ - public CharSequence[] getColorDescriptions() { - return colorDescriptions; - } - - /** - * Sets the available colors' descriptions that can be used by accessibility services. - * - * @param colorDescriptions The available colors' descriptions. - */ - public void setColorDescriptions(CharSequence[] colorDescriptions) { - this.colorDescriptions = colorDescriptions; - } - - /** - * Returns the number of columns to be used in the picker dialog for displaying the available - * colors. If the value is less than or equals to 0, the number of columns will be determined - * automatically by the system using FlexboxLayoutManager. - * - * @return The number of columns to be used in the picker dialog. - * @see com.google.android.flexbox.FlexboxLayoutManager - */ - public int getColumns() { - return columns; - } - - /** - * Sets the number of columns to be used in the picker dialog for displaying the available - * colors. If the value is less than or equals to 0, the number of columns will be determined - * automatically by the system using FlexboxLayoutManager. - * - * @param columns The number of columns to be used in the picker dialog. Use 0 to set it to - * 'auto' mode. - * @see com.google.android.flexbox.FlexboxLayoutManager - */ - public void setColumns(int columns) { - this.columns = columns; - } - - /** - * Returns the size of the color swatches in the dialog. It can be either - * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}. - * - * @return The size of the color swatches in the dialog. - * @see ColorPickerDialog#SIZE_SMALL - * @see ColorPickerDialog#SIZE_LARGE - */ - @Size - public int getSize() { - return size; - } - - /** - * Sets the size of the color swatches in the dialog. It can be either - * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}. - * - * @param size The size of the color swatches in the dialog. It can be either - * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}. - * @see ColorPickerDialog#SIZE_SMALL - * @see ColorPickerDialog#SIZE_LARGE - */ - public void setSize(@Size int size) { - this.size = size; - } - - private void setInternalColor(int color, boolean force) { - int oldColor = getPersistedInt(0); - - boolean changed = oldColor != color; - - if (changed || force) { - this.color = color; - - persistInt(color); - - setColorOnWidget(color); - - if (listener != null) listener.onPreferenceChange(this, color); - notifyChanged(); - } - } - - @Override - protected Object onGetDefaultValue(TypedArray a, int index) { - return a.getString(index); - } - - @Override - protected void onSetInitialValue(boolean restoreValue, Object defaultValueObj) { - final String defaultValue = (String) defaultValueObj; - setInternalColor(restoreValue ? getPersistedInt(0) : (!TextUtils.isEmpty(defaultValue) ? Color.parseColor(defaultValue) : 0), true); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreferenceDialogFragmentCompat.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreferenceDialogFragmentCompat.java deleted file mode 100644 index 964f439ba..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreferenceDialogFragmentCompat.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.preference.PreferenceDialogFragmentCompat; - -import com.takisoft.colorpicker.ColorPickerDialog; -import com.takisoft.colorpicker.OnColorSelectedListener; - -public class ColorPickerPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat implements OnColorSelectedListener { - - private int pickedColor; - - public static ColorPickerPreferenceDialogFragmentCompat newInstance(String key) { - ColorPickerPreferenceDialogFragmentCompat fragment = new ColorPickerPreferenceDialogFragmentCompat(); - Bundle b = new Bundle(1); - b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key); - fragment.setArguments(b); - return fragment; - } - - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ColorPickerPreference pref = getColorPickerPreference(); - - ColorPickerDialog.Params params = new ColorPickerDialog.Params.Builder(getContext()) - .setSelectedColor(pref.getColor()) - .setColors(pref.getColors()) - .setColorContentDescriptions(pref.getColorDescriptions()) - .setSize(pref.getSize()) - .setSortColors(pref.isSortColors()) - .setColumns(pref.getColumns()) - .build(); - - ColorPickerDialog dialog = new ColorPickerDialog(getActivity(), this, params); - dialog.setTitle(pref.getDialogTitle()); - - return dialog; - } - - @Override - public void onDialogClosed(boolean positiveResult) { - ColorPickerPreference preference = getColorPickerPreference(); - - if (positiveResult) { - preference.setColor(pickedColor); - } - } - - @Override - public void onColorSelected(int color) { - this.pickedColor = color; - - super.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); - } - - ColorPickerPreference getColorPickerPreference() { - return (ColorPickerPreference) getPreference(); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ContactPreference.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ContactPreference.java deleted file mode 100644 index f5417f3e9..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ContactPreference.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - - -import android.content.Context; -import android.graphics.PorterDuff; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ImageView; - -import network.loki.messenger.R; - -public class ContactPreference extends Preference { - - private ImageView messageButton; - - private Listener listener; - private boolean secure; - - public ContactPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initialize(); - } - - public ContactPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initialize(); - } - - public ContactPreference(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(); - } - - public ContactPreference(Context context) { - super(context); - initialize(); - } - - private void initialize() { - setWidgetLayoutResource(R.layout.recipient_preference_contact_widget); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - this.messageButton = (ImageView) view.findViewById(R.id.message); - - if (listener != null) setListener(listener); - setSecure(secure); - } - - public void setSecure(boolean secure) { - this.secure = secure; - - int color; - - if (secure) { - color = getContext().getResources().getColor(R.color.textsecure_primary); - } else { - color = getContext().getResources().getColor(R.color.grey_600); - } - - if (messageButton != null) messageButton.setColorFilter(color, PorterDuff.Mode.SRC_IN); - } - - public void setListener(Listener listener) { - this.listener = listener; - - if (this.messageButton != null) this.messageButton.setOnClickListener(v -> listener.onMessageClicked()); - } - - public interface Listener { - public void onMessageClicked(); - public void onSecureCallClicked(); - public void onInSecureCallClicked(); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationSettingsPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationSettingsPreference.kt deleted file mode 100644 index 3c2e72779..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationSettingsPreference.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout - -class NotificationSettingsPreference @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null -) : FrameLayout(context, attrs) { - - override fun onFinishInflate() { - super.onFinishInflate() - // TODO: if we want do the spans - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProgressPreference.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProgressPreference.java deleted file mode 100644 index 52a88c566..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProgressPreference.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - - -import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TextView; - -import network.loki.messenger.R; - -public class ProgressPreference extends Preference { - - private View container; - private TextView progressText; - - public ProgressPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initialize(); - } - - public ProgressPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initialize(); - } - - public ProgressPreference(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(); - } - - public ProgressPreference(Context context) { - super(context); - initialize(); - } - - private void initialize() { - setWidgetLayoutResource(R.layout.preference_widget_progress); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - this.container = view.findViewById(R.id.container); - this.progressText = (TextView) view.findViewById(R.id.progress_text); - - this.container.setVisibility(View.GONE); - } - - public void setProgress(int count) { - container.setVisibility(View.VISIBLE); - progressText.setText(getContext().getString(R.string.ProgressPreference_d_messages_so_far, count)); - } - - public void setProgressVisible(boolean visible) { - container.setVisibility(visible ? View.VISIBLE : View.GONE); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java index f1cbea16c..1c05e68bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java @@ -144,7 +144,6 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter emoji; - - ThisMessageEmojiPageModel(@NonNull List emoji) { - this.emoji = emoji; - } - - @Override - public String getKey() { - return RecentEmojiPageModel.KEY; - } - - @Override - public int getIconAttr() { - return R.attr.emoji_category_recent; - } - - @Override - public @NonNull List getEmoji() { - return emoji; - } - - @Override - public @NonNull List getDisplayEmoji() { - return Stream.of(getEmoji()).map(Emoji::new).toList(); - } - - @Override - public boolean hasSpriteMap() { - return false; - } - - @Override - public @Nullable Uri getSpriteUri() { - return null; - } - - @Override - public boolean isDynamic() { - return true; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index 2d6789401..dd013afa7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -23,9 +23,11 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.SessionJobDatabase import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.ConfigFactory import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -62,7 +64,7 @@ interface ConversationRepository { suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf - suspend fun clearAllMessageRequests(): ResultOf + suspend fun clearAllMessageRequests(block: Boolean): ResultOf suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf @@ -82,8 +84,10 @@ class DefaultConversationRepository @Inject constructor( private val mmsDb: MmsDatabase, private val mmsSmsDb: MmsSmsDatabase, private val recipientDb: RecipientDatabase, + private val storage: Storage, private val lokiMessageDb: LokiMessageDatabase, - private val sessionJobDb: SessionJobDatabase + private val sessionJobDb: SessionJobDatabase, + private val configFactory: ConfigFactory ) : ConversationRepository { override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? { @@ -110,7 +114,7 @@ class DefaultConversationRepository @Inject constructor( val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return for (contact in contacts) { val message = VisibleMessage() - message.sentTimestamp = System.currentTimeMillis() + message.sentTimestamp = SnodeAPI.nowWithOffset val openGroupInvitation = OpenGroupInvitation() openGroupInvitation.name = openGroup.name openGroupInvitation.url = openGroup.joinURL @@ -125,8 +129,9 @@ class DefaultConversationRepository @Inject constructor( } } + // This assumes that recipient.isContactRecipient is true override fun setBlocked(recipient: Recipient, blocked: Boolean) { - recipientDb.setBlocked(recipient, blocked) + storage.setBlocked(listOf(recipient), blocked) } override fun deleteLocally(recipient: Recipient, message: MessageRecord) { @@ -139,7 +144,7 @@ class DefaultConversationRepository @Inject constructor( } override fun setApproved(recipient: Recipient, isApproved: Boolean) { - recipientDb.setApproved(recipient, isApproved) + storage.setRecipientApproved(recipient, isApproved) } override suspend fun deleteForEveryone( @@ -250,29 +255,33 @@ class DefaultConversationRepository @Inject constructor( override suspend fun deleteThread(threadId: Long): ResultOf { sessionJobDb.cancelPendingMessageSendJobs(threadId) - threadDb.deleteConversation(threadId) + storage.deleteConversation(threadId) return ResultOf.Success(Unit) } override suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf { sessionJobDb.cancelPendingMessageSendJobs(thread.threadId) - threadDb.deleteConversation(thread.threadId) + storage.deleteConversation(thread.threadId) return ResultOf.Success(Unit) } - override suspend fun clearAllMessageRequests(): ResultOf { + override suspend fun clearAllMessageRequests(block: Boolean): ResultOf { threadDb.readerFor(threadDb.unapprovedConversationList).use { reader -> while (reader.next != null) { deleteMessageRequest(reader.current) + val recipient = reader.current.recipient + if (block) { + setBlocked(recipient, true) + } } } return ResultOf.Success(Unit) } override suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf = suspendCoroutine { continuation -> - recipientDb.setApproved(recipient, true) + storage.setRecipientApproved(recipient, true) val message = MessageRequestResponse(true) - MessageSender.send(message, Destination.from(recipient.address)) + MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = recipient.isLocalNumber) .success { threadDb.setHasSent(threadId, true) continuation.resume(ResultOf.Success(Unit)) @@ -283,7 +292,7 @@ class DefaultConversationRepository @Inject constructor( override fun declineMessageRequest(threadId: Long) { sessionJobDb.cancelPendingMessageSendJobs(threadId) - threadDb.deleteConversation(threadId) + storage.deleteConversation(threadId) } override fun hasReceived(threadId: Long): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java index f42b55b5f..85d8c8f43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.service; import android.content.Context; import org.jetbrains.annotations.NotNull; +import org.session.libsession.database.StorageProtocol; +import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; import org.session.libsession.messaging.messages.signal.IncomingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage; @@ -15,6 +17,7 @@ import org.session.libsignal.messages.SignalServiceGroup; import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.guava.Optional; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; @@ -35,12 +38,14 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM private final SmsDatabase smsDatabase; private final MmsDatabase mmsDatabase; + private final MmsSmsDatabase mmsSmsDatabase; private final Context context; public ExpiringMessageManager(Context context) { this.context = context.getApplicationContext(); this.smsDatabase = DatabaseComponent.get(context).smsDatabase(); this.mmsDatabase = DatabaseComponent.get(context).mmsDatabase(); + this.mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); executor.execute(new LoadTask()); executor.execute(new ProcessTask()); @@ -79,12 +84,11 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM } if (message.getId() != null) { - DatabaseComponent.get(context).smsDatabase().deleteMessage(message.getId()); + smsDatabase.deleteMessage(message.getId()); } } private void insertIncomingExpirationTimerMessage(ExpirationTimerUpdate message) { - MmsDatabase database = DatabaseComponent.get(context).mmsDatabase(); String senderPublicKey = message.getSender(); Long sentTimestamp = message.getSentTimestamp(); @@ -106,6 +110,10 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM Address groupAddress = Address.fromSerialized(groupID); recipient = Recipient.from(context, groupAddress, false); } + Long threadId = MessagingModuleConfiguration.getShared().getStorage().getThreadId(recipient); + if (threadId == null) { + return; + } IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1, duration * 1000L, true, @@ -120,10 +128,10 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM Optional.absent(), Optional.absent()); //insert the timer update message - database.insertSecureDecryptedMessageInbox(mediaMessage, -1, true, true); + mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, threadId, true); //set the timer to the conversation - DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration); + MessagingModuleConfiguration.getShared().getStorage().setExpirationTimer(recipient.getAddress().serialize(), duration); } catch (IOException | MmsException ioe) { Log.e("Loki", "Failed to insert expiration update message."); @@ -131,28 +139,30 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM } private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message) { - MmsDatabase database = DatabaseComponent.get(context).mmsDatabase(); Long sentTimestamp = message.getSentTimestamp(); String groupId = message.getGroupPublicKey(); int duration = message.getDuration(); - Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient()); - Recipient recipient = Recipient.from(context, address, false); + Address address; try { - OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId); - database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp, true); - if (groupId != null) { - // we need the group ID as recipient for setExpireMessages below - recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)), false); + address = Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)); + } else { + address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient()); } - //set the timer to the conversation - DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration); + Recipient recipient = Recipient.from(context, address, false); + StorageProtocol storage = MessagingModuleConfiguration.getShared().getStorage(); + message.setThreadID(storage.getOrCreateThreadIdFor(address)); + + OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId); + mmsDatabase.insertSecureDecryptedMessageOutbox(timerUpdateMessage, message.getThreadID(), sentTimestamp, true); + //set the timer to the conversation + MessagingModuleConfiguration.getShared().getStorage().setExpirationTimer(recipient.getAddress().serialize(), duration); } catch (MmsException | IOException ioe) { - Log.e("Loki", "Failed to insert expiration update message."); + Log.e("Loki", "Failed to insert expiration update message.", ioe); } } @@ -163,7 +173,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM @Override public void startAnyExpiration(long timestamp, @NotNull String author) { - MessageRecord messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageFor(timestamp, author); + MessageRecord messageRecord = mmsSmsDatabase.getMessageFor(timestamp, author); if (messageRecord != null) { boolean mms = messageRecord.isMms(); Recipient recipient = messageRecord.getRecipient(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java deleted file mode 100644 index 0b1b3f842..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.thoughtcrime.securesms.service; - -import android.content.Context; -import android.content.Intent; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.jobs.LocalBackupJob; -import org.session.libsession.utilities.TextSecurePreferences; - -import java.util.concurrent.TimeUnit; - -public class LocalBackupListener extends PersistentAlarmManagerListener { - - private static final long INTERVAL = TimeUnit.DAYS.toMillis(1); - - @Override - protected long getNextScheduledExecutionTime(Context context) { - return TextSecurePreferences.getNextBackupTime(context); - } - - @Override - protected long onAlarm(Context context, long scheduledTime) { - if (TextSecurePreferences.isBackupEnabled(context)) { - ApplicationContext.getInstance(context).getJobManager().add(new LocalBackupJob()); - } - - long nextTime = System.currentTimeMillis() + INTERVAL; - TextSecurePreferences.setNextBackupTime(context, nextTime); - - return nextTime; - } - - public static void schedule(Context context) { - if (TextSecurePreferences.isBackupEnabled(context)) { - new LocalBackupListener().onReceive(context, new Intent()); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java b/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java index 0f5a3f17a..a56bc8c0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java @@ -9,6 +9,7 @@ import android.widget.Toast; import network.loki.messenger.R; import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsignal.utilities.Log; import org.session.libsession.messaging.sending_receiving.MessageSender; @@ -50,7 +51,7 @@ public class QuickResponseService extends IntentService { if (!TextUtils.isEmpty(content)) { VisibleMessage message = new VisibleMessage(); message.setText(content); - message.setSentTimestamp(System.currentTimeMillis()); + message.setSentTimestamp(SnodeAPI.getNowWithOffset()); MessageSender.send(message, Address.fromExternal(this, number)); } } catch (URISyntaxException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java deleted file mode 100644 index 187713df9..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.thoughtcrime.securesms.service; - - -import android.content.Context; -import android.content.Intent; -import org.session.libsignal.utilities.Log; - -import org.thoughtcrime.securesms.ApplicationContext; -import network.loki.messenger.BuildConfig; -import org.thoughtcrime.securesms.jobs.UpdateApkJob; -import org.session.libsession.utilities.TextSecurePreferences; - -import java.util.concurrent.TimeUnit; - -public class UpdateApkRefreshListener extends PersistentAlarmManagerListener { - - private static final String TAG = UpdateApkRefreshListener.class.getSimpleName(); - - private static final long INTERVAL = TimeUnit.HOURS.toMillis(6); - - @Override - protected long getNextScheduledExecutionTime(Context context) { - return TextSecurePreferences.getUpdateApkRefreshTime(context); - } - - @Override - protected long onAlarm(Context context, long scheduledTime) { - Log.i(TAG, "onAlarm..."); - - if (scheduledTime != 0 && BuildConfig.PLAY_STORE_DISABLED) { - Log.i(TAG, "Queueing APK update job..."); - ApplicationContext.getInstance(context) - .getJobManager() - .add(new UpdateApkJob()); - } - - long newTime = System.currentTimeMillis() + INTERVAL; - TextSecurePreferences.setUpdateApkRefreshTime(context, newTime); - - return newTime; - } - - public static void schedule(Context context) { - new UpdateApkRefreshListener().onReceive(context, new Intent()); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt index d09933ab8..3ae3d30f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -1,6 +1,6 @@ package org.thoughtcrime.securesms.service -import android.app.Service +import android.app.ForegroundServiceStartNotAllowedException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -17,6 +17,8 @@ import android.telephony.PhoneStateListener.LISTEN_NONE import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import androidx.core.os.bundleOf +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.messaging.calls.CallMessageType @@ -25,6 +27,7 @@ import org.session.libsession.utilities.FutureTaskListener import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.calls.WebRtcCallActivity +import org.thoughtcrime.securesms.notifications.BackgroundPollWorker import org.thoughtcrime.securesms.util.CallNotificationBuilder import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_ESTABLISHED import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_CONNECTING @@ -46,7 +49,7 @@ import javax.inject.Inject import org.thoughtcrime.securesms.webrtc.data.State as CallState @AndroidEntryPoint -class WebRtcCallService : Service(), CallManager.WebRtcListener { +class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener { companion object { @@ -238,7 +241,10 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { scheduledReconnect?.cancel(false) scheduledTimeout = null scheduledReconnect = null - stopForeground(true) + + lifecycleScope.launchWhenCreated { + stopForeground(true) + } } private fun isSameCall(intent: Intent): Boolean { @@ -253,7 +259,9 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { private fun isIdle() = callManager.isIdle() - override fun onBind(intent: Intent?): IBinder? = null + override fun onBind(intent: Intent): IBinder? { + return super.onBind(intent) + } override fun onHangup() { serviceExecutor.execute { @@ -272,7 +280,8 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { if (intent == null || intent.action == null) return START_NOT_STICKY serviceExecutor.execute { val action = intent.action - Log.i("Loki", "Handling ${intent.action}") + val callId = ((intent.getSerializableExtra(EXTRA_CALL_ID) as? UUID)?.toString() ?: "No callId") + Log.i("Loki", "Handling ${intent.action} for call: ${callId}") when { action == ACTION_INCOMING_RING && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleNewOffer( intent @@ -361,7 +370,9 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { insertMissedCall(recipient, false) if (callState == CallState.Idle) { - stopForeground(true) + lifecycleScope.launchWhenCreated { + stopForeground(true) + } } } @@ -409,6 +420,11 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { callManager.initializeAudioForCall() callManager.startIncomingRinger() callManager.setAudioEnabled(true) + + BackgroundPollWorker.scheduleOnce( + this, + arrayOf(BackgroundPollWorker.Targets.DMS) + ) } } @@ -573,7 +589,9 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { private fun handleRemoteHangup(intent: Intent) { if (callManager.callId != getCallId(intent)) { Log.e(TAG, "Hangup for non-active call...") - stopForeground(true) + lifecycleScope.launchWhenCreated { + stopForeground(true) + } return } @@ -717,10 +735,16 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { } private fun setCallInProgressNotification(type: Int, recipient: Recipient?) { - startForeground( - CallNotificationBuilder.WEBRTC_NOTIFICATION, - CallNotificationBuilder.getCallInProgressNotification(this, type, recipient) - ) + try { + startForeground( + CallNotificationBuilder.WEBRTC_NOTIFICATION, + CallNotificationBuilder.getCallInProgressNotification(this, type, recipient) + ) + } + catch(e: ForegroundServiceStartNotAllowedException) { + Log.e(TAG, "Failed to setCallInProgressNotification as a foreground service for type: ${type}, trying to update instead") + } + if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) { // start an intent for the fullscreen val foregroundIntent = Intent(this, WebRtcCallActivity::class.java) @@ -769,10 +793,15 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { callReceiver?.let { receiver -> unregisterReceiver(receiver) } + wiredHeadsetStateReceiver?.let { unregisterReceiver(it) } + powerButtonReceiver?.let { unregisterReceiver(it) } networkChangedReceiver?.unregister(this) wantsToAnswerReceiver?.let { receiver -> LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } + callManager.shutDownAudioManager() + powerButtonReceiver = null + wiredHeadsetStateReceiver = null networkChangedReceiver = null callReceiver = null uncaughtExceptionHandlerManager?.unregister() diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/TelephonyServiceState.java b/app/src/main/java/org/thoughtcrime/securesms/sms/TelephonyServiceState.java deleted file mode 100644 index b6cb7e5e2..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/TelephonyServiceState.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.thoughtcrime.securesms.sms; - -import android.content.Context; -import android.os.Looper; -import android.telephony.PhoneStateListener; -import android.telephony.ServiceState; -import android.telephony.TelephonyManager; - -public class TelephonyServiceState { - - public boolean isConnected(Context context) { - ListenThread listenThread = new ListenThread(context); - listenThread.start(); - - return listenThread.get(); - } - - private static class ListenThread extends Thread { - - private final Context context; - - private boolean complete; - private boolean result; - - public ListenThread(Context context) { - this.context = context.getApplicationContext(); - } - - @Override - public void run() { - Looper looper = initializeLooper(); - ListenCallback callback = new ListenCallback(looper); - - TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); - telephonyManager.listen(callback, PhoneStateListener.LISTEN_SERVICE_STATE); - - Looper.loop(); - - telephonyManager.listen(callback, PhoneStateListener.LISTEN_NONE); - - set(callback.isConnected()); - } - - private Looper initializeLooper() { - Looper looper = Looper.myLooper(); - - if (looper == null) { - Looper.prepare(); - } - - return Looper.myLooper(); - } - - public synchronized boolean get() { - while (!complete) { - try { - wait(); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - } - - return result; - } - - private synchronized void set(boolean result) { - this.result = result; - this.complete = true; - notifyAll(); - } - } - - private static class ListenCallback extends PhoneStateListener { - - private final Looper looper; - private volatile boolean connected; - - public ListenCallback(Looper looper) { - this.looper = looper; - } - - @Override - public void onServiceStateChanged(ServiceState serviceState) { - this.connected = (serviceState.getState() == ServiceState.STATE_IN_SERVICE); - looper.quit(); - } - - public boolean isConnected() { - return connected; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt index c46f75bff..8b1975865 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt @@ -1,16 +1,23 @@ package org.thoughtcrime.securesms.sskenvironment import android.content.Context +import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob +import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.utilities.SSKEnvironment +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient -import org.thoughtcrime.securesms.ApplicationContext +import org.session.libsignal.utilities.IdPrefix +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -class ProfileManager : SSKEnvironment.ProfileManagerProtocol { +class ProfileManager(private val context: Context, private val configFactory: ConfigFactory) : SSKEnvironment.ProfileManagerProtocol { override fun setNickname(context: Context, recipient: Recipient, nickname: String?) { + if (recipient.isLocalNumber) return val sessionID = recipient.address.serialize() val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase() var contact = contactDatabase.getContactWithSessionID(sessionID) @@ -20,10 +27,12 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol { contact.nickname = nickname contactDatabase.setContact(contact) } + contactUpdatedInternal(contact) } - override fun setName(context: Context, recipient: Recipient, name: String) { + override fun setName(context: Context, recipient: Recipient, name: String?) { // New API + if (recipient.isLocalNumber) return val sessionID = recipient.address.serialize() val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase() var contact = contactDatabase.getContactWithSessionID(sessionID) @@ -37,41 +46,69 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol { val database = DatabaseComponent.get(context).recipientDatabase() database.setProfileName(recipient, name) recipient.notifyListeners() + contactUpdatedInternal(contact) } - override fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) { - val job = RetrieveProfileAvatarJob(recipient, profilePictureURL) - val jobManager = ApplicationContext.getInstance(context).jobManager - jobManager.add(job) + override fun setProfilePicture( + context: Context, + recipient: Recipient, + profilePictureURL: String?, + profileKey: ByteArray? + ) { + val hasPendingDownload = DatabaseComponent + .get(context) + .sessionJobDatabase() + .getAllJobs(RetrieveProfileAvatarJob.KEY).any { + (it.value as? RetrieveProfileAvatarJob)?.recipientAddress == recipient.address + } + val resolved = recipient.resolve() + DatabaseComponent.get(context).storage().setProfilePicture( + recipient = resolved, + newProfileKey = profileKey, + newProfilePicture = profilePictureURL + ) val sessionID = recipient.address.serialize() val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase() var contact = contactDatabase.getContactWithSessionID(sessionID) if (contact == null) contact = Contact(sessionID) contact.threadID = DatabaseComponent.get(context).storage().getThreadId(recipient.address) - if (contact.profilePictureURL != profilePictureURL) { + if (!contact.profilePictureEncryptionKey.contentEquals(profileKey) || contact.profilePictureURL != profilePictureURL) { + contact.profilePictureEncryptionKey = profileKey contact.profilePictureURL = profilePictureURL contactDatabase.setContact(contact) } - } - - override fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray) { - // New API - val sessionID = recipient.address.serialize() - val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase() - var contact = contactDatabase.getContactWithSessionID(sessionID) - if (contact == null) contact = Contact(sessionID) - contact.threadID = DatabaseComponent.get(context).storage().getThreadId(recipient.address) - if (!contact.profilePictureEncryptionKey.contentEquals(profileKey)) { - contact.profilePictureEncryptionKey = profileKey - contactDatabase.setContact(contact) + contactUpdatedInternal(contact) + if (!hasPendingDownload) { + val job = RetrieveProfileAvatarJob(profilePictureURL, recipient.address) + JobQueue.shared.add(job) } - // Old API - val database = DatabaseComponent.get(context).recipientDatabase() - database.setProfileKey(recipient, profileKey) } override fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode) { val database = DatabaseComponent.get(context).recipientDatabase() database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode) } + + override fun contactUpdatedInternal(contact: Contact): String? { + val contactConfig = configFactory.contacts ?: return null + if (contact.sessionID == TextSecurePreferences.getLocalNumber(context)) return null + val sessionId = SessionId(contact.sessionID) + if (sessionId.prefix != IdPrefix.STANDARD) return null // only internally store standard session IDs + contactConfig.upsertContact(contact.sessionID) { + this.name = contact.name.orEmpty() + this.nickname = contact.nickname.orEmpty() + val url = contact.profilePictureURL + val key = contact.profilePictureEncryptionKey + if (!url.isNullOrEmpty() && key != null && key.size == 32) { + this.profilePicture = UserPic(url, key) + } else if (url.isNullOrEmpty() && key == null) { + this.profilePicture = UserPic.DEFAULT + } + } + if (contactConfig.needsPush()) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + } + return contactConfig.get(contact.sessionID)?.hashCode()?.toString() + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt new file mode 100644 index 000000000..55bc1be62 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -0,0 +1,63 @@ +package org.thoughtcrime.securesms.ui + +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Colors +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +val colorDestructive = Color(0xffFF453A) + +const val classicDark0 = 0xff111111 +const val classicDark1 = 0xff1B1B1B +const val classicDark2 = 0xff2D2D2D +const val classicDark3 = 0xff414141 +const val classicDark4 = 0xff767676 +const val classicDark5 = 0xffA1A2A1 +const val classicDark6 = 0xffFFFFFF + +const val classicLight0 = 0xff000000 +const val classicLight1 = 0xff6D6D6D +const val classicLight2 = 0xffA1A2A1 +const val classicLight3 = 0xffDFDFDF +const val classicLight4 = 0xffF0F0F0 +const val classicLight5 = 0xffF9F9F9 +const val classicLight6 = 0xffFFFFFF + +const val oceanDark0 = 0xff000000 +const val oceanDark1 = 0xff1A1C28 +const val oceanDark2 = 0xff252735 +const val oceanDark3 = 0xff2B2D40 +const val oceanDark4 = 0xff3D4A5D +const val oceanDark5 = 0xffA6A9CE +const val oceanDark6 = 0xff5CAACC +const val oceanDark7 = 0xffFFFFFF + +const val oceanLight0 = 0xff000000 +const val oceanLight1 = 0xff19345D +const val oceanLight2 = 0xff6A6E90 +const val oceanLight3 = 0xff5CAACC +const val oceanLight4 = 0xffB3EDF2 +const val oceanLight5 = 0xffE7F3F4 +const val oceanLight6 = 0xffECFAFB +const val oceanLight7 = 0xffFCFFFF + +val ocean_accent = Color(0xff57C9FA) + +val oceanLights = arrayOf(oceanLight0, oceanLight1, oceanLight2, oceanLight3, oceanLight4, oceanLight5, oceanLight6, oceanLight7) +val oceanDarks = arrayOf(oceanDark0, oceanDark1, oceanDark2, oceanDark3, oceanDark4, oceanDark5, oceanDark6, oceanDark7) +val classicLights = arrayOf(classicLight0, classicLight1, classicLight2, classicLight3, classicLight4, classicLight5, classicLight6) +val classicDarks = arrayOf(classicDark0, classicDark1, classicDark2, classicDark3, classicDark4, classicDark5, classicDark6) + +val oceanLightColors = oceanLights.map(::Color) +val oceanDarkColors = oceanDarks.map(::Color) +val classicLightColors = classicLights.map(::Color) +val classicDarkColors = classicDarks.map(::Color) + +val blackAlpha40 = Color.Black.copy(alpha = 0.4f) + +@Composable +fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent) + +@Composable +fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt new file mode 100644 index 000000000..1724bde8a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -0,0 +1,182 @@ +package org.thoughtcrime.securesms.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonColors +import androidx.compose.material.Card +import androidx.compose.material.Colors +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.google.accompanist.pager.HorizontalPagerIndicator +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.components.ProfilePictureView + +@Composable +fun ItemButton( + text: String, + @DrawableRes icon: Int, + colors: ButtonColors = transparentButtonColors(), + contentDescription: String = text, + onClick: () -> Unit +) { + TextButton( + modifier = Modifier + .fillMaxWidth() + .height(60.dp), + colors = colors, + onClick = onClick, + shape = RectangleShape, + ) { + Box(modifier = Modifier + .width(80.dp) + .fillMaxHeight()) { + Icon( + painter = painterResource(id = icon), + contentDescription = contentDescription, + modifier = Modifier.align(Alignment.Center) + ) + } + Text(text, modifier = Modifier.fillMaxWidth()) + } +} + +@Composable +fun Cell(content: @Composable () -> Unit) { + CellWithPaddingAndMargin(padding = 0.dp) { content() } +} +@Composable +fun CellNoMargin(content: @Composable () -> Unit) { + CellWithPaddingAndMargin(padding = 0.dp, margin = 0.dp) { content() } +} + +@Composable +fun CellWithPaddingAndMargin( + padding: Dp = 24.dp, + margin: Dp = 32.dp, + content: @Composable () -> Unit +) { + Card( + backgroundColor = MaterialTheme.colors.cellColor, + shape = RoundedCornerShape(16.dp), + elevation = 0.dp, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = margin), + ) { + Box(Modifier.padding(padding)) { content() } + } +} + +private val Colors.cellColor: Color + @Composable + get() = LocalExtraColors.current.settingsBackground + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) { + if (pagerState.pageCount >= 2) Card( + shape = RoundedCornerShape(50.dp), + backgroundColor = Color.Black.copy(alpha = 0.4f), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(8.dp) + ) { + Box(modifier = Modifier.padding(8.dp)) { + HorizontalPagerIndicator( + pagerState = pagerState, + pageCount = pagerState.pageCount, + activeColor = Color.White, + inactiveColor = classicDarkColors[5]) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RowScope.CarouselPrevButton(pagerState: PagerState) { + CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RowScope.CarouselNextButton(pagerState: PagerState) { + CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RowScope.CarouselButton( + pagerState: PagerState, + enabled: Boolean, + @DrawableRes id: Int, + delta: Int +) { + if (pagerState.pageCount <= 1) Spacer(modifier = Modifier.width(32.dp)) + else { + val animationScope = rememberCoroutineScope() + IconButton( + modifier = Modifier + .width(40.dp) + .align(Alignment.CenterVertically), + enabled = enabled, + onClick = { animationScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + delta) } }) { + Icon( + painter = painterResource(id = id), + contentDescription = "", + ) + } + } +} + +@Composable +fun Divider() { + androidx.compose.material.Divider( + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +fun RowScope.Avatar(recipient: Recipient) { + Box( + modifier = Modifier + .width(60.dp) + .align(Alignment.CenterVertically) + ) { + AndroidView( + factory = { + ProfilePictureView(it).apply { update(recipient) } + }, + modifier = Modifier + .width(46.dp) + .height(46.dp) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt new file mode 100644 index 000000000..44ff4a42d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.ui + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource + +/** + * Compatibility class to allow ViewModels to use strings and string resources interchangeably. + */ +sealed class GetString { + @Composable + abstract fun string(): String + data class FromString(val string: String): GetString() { + @Composable + override fun string(): String = string + } + data class FromResId(@StringRes val resId: Int): GetString() { + @Composable + override fun string(): String = stringResource(resId) + + } +} + +fun GetString(@StringRes resId: Int) = GetString.FromResId(resId) +fun GetString(string: String) = GetString.FromString(string) + + +/** + * Represents some text with an associated title. + */ +data class TitledText(val title: GetString, val text: String) { + constructor(title: String, text: String): this(GetString(title), text) + constructor(@StringRes title: Int, text: String): this(GetString(title), text) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt new file mode 100644 index 000000000..64bbd21d8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -0,0 +1,76 @@ +package org.thoughtcrime.securesms.ui + +import android.content.Context +import androidx.annotation.AttrRes +import androidx.appcompat.view.ContextThemeWrapper +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import com.google.android.material.color.MaterialColors +import network.loki.messenger.R + +val LocalExtraColors = staticCompositionLocalOf { error("No Custom Attribute value provided") } + + +data class ExtraColors( + val settingsBackground: Color, +) + +/** + * Converts current Theme to Compose Theme. + */ +@Composable +fun AppTheme( + content: @Composable () -> Unit +) { + val extraColors = LocalContext.current.run { + ExtraColors( + settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground), + ) + } + + CompositionLocalProvider(LocalExtraColors provides extraColors) { + AppCompatTheme { + content() + } + } +} + +fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color = + MaterialColors.getColor(this, attr, defaultValue).let(::Color) + +/** + * Set the theme and a background for Compose Previews. + */ +@Composable +fun PreviewTheme( + themeResId: Int, + content: @Composable () -> Unit +) { + CompositionLocalProvider( + LocalContext provides ContextThemeWrapper(LocalContext.current, themeResId) + ) { + AppTheme { + Box(modifier = Modifier.background(color = MaterialTheme.colors.background)) { + content() + } + } + } +} + +class ThemeResPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + R.style.Classic_Dark, + R.style.Classic_Light, + R.style.Ocean_Dark, + R.style.Ocean_Light, + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt index d5b361ecd..5ff823a15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt @@ -7,10 +7,10 @@ import android.view.View import androidx.annotation.StyleRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.DialogFragment import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog fun BaseActionBarActivity.setUpActionBarSessionLogo(hideBackButton: Boolean = false) { val actionbar = supportActionBar!! @@ -66,7 +66,7 @@ interface ActivityDispatcher { fun get(context: Context) = context.getSystemService(SERVICE) as? ActivityDispatcher } fun dispatchIntent(body: (Context)->Intent?) - fun showDialog(baseDialog: BaseDialog, tag: String? = null) + fun showDialog(dialogFragment: DialogFragment, tag: String? = null) } fun TextSecurePreferences.themeState(): ThemeState { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt deleted file mode 100644 index 074278cb9..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt +++ /dev/null @@ -1,313 +0,0 @@ -package org.thoughtcrime.securesms.util - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.DocumentsContract -import android.widget.Toast -import androidx.annotation.WorkerThread -import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.Fragment -import network.loki.messenger.R -import org.greenrobot.eventbus.EventBus -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.ByteUtil -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.backup.BackupEvent -import org.thoughtcrime.securesms.backup.BackupPassphrase -import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference -import org.thoughtcrime.securesms.backup.FullBackupExporter -import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil -import org.thoughtcrime.securesms.database.BackupFileRecord -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import java.io.IOException -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.security.SecureRandom -import java.text.SimpleDateFormat -import java.util.* - -object BackupUtil { - private const val MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences" - private const val TAG = "BackupUtil" - const val BACKUP_FILE_MIME_TYPE = "application/session-backup" - const val BACKUP_PASSPHRASE_LENGTH = 30 - - fun getBackupRecords(context: Context): List { - val prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME - val preferences = context.getSharedPreferences(prefName, 0) - val prefList = LinkedList() - prefList.add(SharedPreference.newBuilder() - .setFile(prefName) - .setKey(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF) - .setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, null)) - .build()) - prefList.add(SharedPreference.newBuilder() - .setFile(prefName) - .setKey(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF) - .setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, null)) - .build()) - if (preferences.contains(IdentityKeyUtil.ED25519_PUBLIC_KEY)) { - prefList.add(SharedPreference.newBuilder() - .setFile(prefName) - .setKey(IdentityKeyUtil.ED25519_PUBLIC_KEY) - .setValue(preferences.getString(IdentityKeyUtil.ED25519_PUBLIC_KEY, null)) - .build()) - } - if (preferences.contains(IdentityKeyUtil.ED25519_SECRET_KEY)) { - prefList.add(SharedPreference.newBuilder() - .setFile(prefName) - .setKey(IdentityKeyUtil.ED25519_SECRET_KEY) - .setValue(preferences.getString(IdentityKeyUtil.ED25519_SECRET_KEY, null)) - .build()) - } - prefList.add(SharedPreference.newBuilder() - .setFile(prefName) - .setKey(IdentityKeyUtil.LOKI_SEED) - .setValue(preferences.getString(IdentityKeyUtil.LOKI_SEED, null)) - .build()) - return prefList - } - - @JvmStatic - fun getLastBackupTimeString(context: Context, locale: Locale): String { - val timestamp = DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFileTime() - if (timestamp == null) { - return context.getString(R.string.BackupUtil_never) - } - return DateUtils.getDisplayFormattedTimeSpanString(context, locale, timestamp.time) - } - - @JvmStatic - fun getLastBackup(context: Context): BackupFileRecord? { - return DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFile() - } - - @JvmStatic - fun generateBackupPassphrase(): Array { - val random = ByteArray(BACKUP_PASSPHRASE_LENGTH).also { SecureRandom().nextBytes(it) } - return Array(6) { i -> - String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000) - } - } - - @JvmStatic - fun validateDirAccess(context: Context, dirUri: Uri): Boolean { - val hasWritePermission = context.contentResolver.persistedUriPermissions.any { - it.isWritePermission && it.uri == dirUri - } - if (!hasWritePermission) return false - - val document = DocumentFile.fromTreeUri(context, dirUri) - if (document == null || !document.exists()) { - return false - } - - return true - } - - @JvmStatic - fun getBackupDirUri(context: Context): Uri? { - val dirUriString = TextSecurePreferences.getBackupSaveDir(context) ?: return null - return Uri.parse(dirUriString) - } - - @JvmStatic - fun setBackupDirUri(context: Context, uriString: String?) { - TextSecurePreferences.setBackupSaveDir(context, uriString) - } - - /** - * @return The selected backup directory if it's valid (exists, is writable). - */ - @JvmStatic - fun getSelectedBackupDirIfValid(context: Context): Uri? { - val dirUri = getBackupDirUri(context) - - if (dirUri == null) { - Log.v(TAG, "The backup dir wasn't selected yet.") - return null - } - if (!validateDirAccess(context, dirUri)) { - Log.v(TAG, "Cannot validate the access to the dir $dirUri.") - return null - } - - return dirUri; - } - - @JvmStatic - @WorkerThread - @Throws(IOException::class) - fun createBackupFile(context: Context): BackupFileRecord { - val backupPassword = BackupPassphrase.get(context) - ?: throw IOException("Backup password is null") - - val dirUri = getSelectedBackupDirIfValid(context) - ?: throw IOException("Backup save directory is not selected or invalid") - - val date = Date() - val timestamp = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(date) - val fileName = String.format("session-%s.backup", timestamp) - - val fileUri = DocumentsContract.createDocument( - context.contentResolver, - DocumentFile.fromTreeUri(context, dirUri)!!.uri, - BACKUP_FILE_MIME_TYPE, - fileName) - - if (fileUri == null) { - Toast.makeText(context, "Cannot create writable file in the dir $dirUri", Toast.LENGTH_LONG).show() - throw IOException("Cannot create writable file in the dir $dirUri") - } - - try { - FullBackupExporter.export(context, - AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret, - DatabaseComponent.get(context).openHelper().readableDatabase, - fileUri, - backupPassword) - } catch (e: Exception) { - // Delete the backup file on any error. - DocumentsContract.deleteDocument(context.contentResolver, fileUri) - throw e - } - - //TODO Use real file size. - val record = DatabaseComponent.get(context).lokiBackupFilesDatabase() - .insertBackupFile(BackupFileRecord(fileUri, -1, date)) - - Log.v(TAG, "A backup file was created: $fileUri") - - return record - } - - @JvmStatic - @JvmOverloads - fun deleteAllBackupFiles(context: Context, except: Collection? = null) { - val db = DatabaseComponent.get(context).lokiBackupFilesDatabase() - db.getBackupFiles().iterator().forEach { record -> - if (except != null && except.contains(record)) return@forEach - - // Try to delete the related file. The operation may fail in many cases - // (the user moved/deleted the file, revoked the write permission, etc), so that's OK. - try { - val result = DocumentsContract.deleteDocument(context.contentResolver, record.uri) - if (!result) { - Log.w(TAG, "Failed to delete backup file: ${record.uri}") - } - } catch (e: Exception) { - Log.w(TAG, "Failed to delete backup file: ${record.uri}", e) - } - - db.deleteBackupFile(record) - - Log.v(TAG, "Backup file was deleted: ${record.uri}") - } - } - - @JvmStatic - fun computeBackupKey(passphrase: String, salt: ByteArray?): ByteArray { - return try { - EventBus.getDefault().post(BackupEvent.createProgress(0)) - val digest = MessageDigest.getInstance("SHA-512") - val input = passphrase.replace(" ", "").toByteArray() - var hash: ByteArray = input - if (salt != null) digest.update(salt) - for (i in 0..249999) { - if (i % 1000 == 0) EventBus.getDefault().post(BackupEvent.createProgress(0)) - digest.update(hash) - hash = digest.digest(input) - } - ByteUtil.trim(hash, 32) - } catch (e: NoSuchAlgorithmException) { - throw AssertionError(e) - } - } -} - -/** - * An utility class to help perform backup directory selection requests. - * - * An instance of this class should be created per an [Activity] or [Fragment] - * and [onActivityResult] should be called appropriately. - */ -class BackupDirSelector(private val contextProvider: ContextProvider) { - - companion object { - private const val REQUEST_CODE_SAVE_DIR = 7844 - } - - private val context: Context get() = contextProvider.getContext() - - private var listener: Listener? = null - - constructor(activity: Activity) : - this(ActivityContextProvider(activity)) - - constructor(fragment: Fragment) : - this(FragmentContextProvider(fragment)) - - /** - * Performs ACTION_OPEN_DOCUMENT_TREE intent to select backup directory URI. - * If the directory is already selected and valid, the request will be skipped. - * @param force if true, the previous selection is ignored and the user is requested to select another directory. - * @param onSelectedListener an optional action to perform once the directory is selected. - */ - fun selectBackupDir(force: Boolean, onSelectedListener: Listener? = null) { - if (!force) { - val dirUri = BackupUtil.getSelectedBackupDirIfValid(context) - if (dirUri != null && onSelectedListener != null) { - onSelectedListener.onBackupDirSelected(dirUri) - } - return - } - - // Let user pick the dir. - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - - // Request read/write permission grant for the dir. - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - - // Set the default dir. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val dirUri = BackupUtil.getBackupDirUri(context) - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, dirUri - ?: Uri.fromFile(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS))) - } - - if (onSelectedListener != null) { - this.listener = onSelectedListener - } - - contextProvider.startActivityForResult(intent, REQUEST_CODE_SAVE_DIR) - } - - fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode != REQUEST_CODE_SAVE_DIR) return - - if (resultCode == Activity.RESULT_OK && data != null && data.data != null) { - // Acquire persistent access permissions for the file selected. - val persistentFlags: Int = data.flags and - (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - context.contentResolver.takePersistableUriPermission(data.data!!, persistentFlags) - - BackupUtil.setBackupDirUri(context, data.dataString) - - listener?.onBackupDirSelected(data.data!!) - } - - listener = null - } - - @FunctionalInterface - interface Listener { - fun onBackupDirSelected(uri: Uri) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt index 56c0a55dd..0ba63fc54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt @@ -1,12 +1,10 @@ package org.thoughtcrime.securesms.util import android.app.Notification -import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.os.Build import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.app.NotificationCompat @@ -32,15 +30,7 @@ class CallNotificationBuilder { @JvmStatic fun areNotificationsEnabled(context: Context): Boolean { val notificationManager = NotificationManagerCompat.from(context) - return when { - !notificationManager.areNotificationsEnabled() -> false - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { - notificationManager.notificationChannels.firstOrNull { channel -> - channel.importance == NotificationManager.IMPORTANCE_NONE - } == null - } - else -> true - } + return notificationManager.areNotificationsEnabled() } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt index fd462417d..297014d86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt @@ -1,18 +1,66 @@ package org.thoughtcrime.securesms.util import android.content.Context +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.UserGroupsConfig +import network.loki.messenger.libsession_util.UserProfile +import network.loki.messenger.libsession_util.util.BaseCommunityInfo +import network.loki.messenger.libsession_util.util.Contact +import network.loki.messenger.libsession_util.util.ExpiryMode +import network.loki.messenger.libsession_util.util.GroupInfo +import network.loki.messenger.libsession_util.util.UserPic import nl.komponents.kovenant.Promise +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.jobs.ConfigurationSyncJob +import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.control.ConfigurationMessage 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.GroupUtil import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.WindowDebouncer +import org.session.libsignal.crypto.ecc.DjbECPublicKey +import org.session.libsignal.utilities.Hex +import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.toHexString +import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import java.util.Timer object ConfigurationMessageUtilities { + private val debouncer = WindowDebouncer(3000, Timer()) + + private fun scheduleConfigSync(userPublicKey: String) { + debouncer.publish { + // don't schedule job if we already have one + val storage = MessagingModuleConfiguration.shared.storage + val ourDestination = Destination.Contact(userPublicKey) + val currentStorageJob = storage.getConfigSyncJob(ourDestination) + if (currentStorageJob != null) { + (currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true) + return@publish + } + val newConfigSync = ConfigurationSyncJob(ourDestination) + JobQueue.shared.add(newConfigSync) + } + } + @JvmStatic fun syncConfigurationIfNeeded(context: Context) { + // add if check here to schedule new config job process and return early val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return + val forcedConfig = TextSecurePreferences.hasForcedNewConfig(context) + val currentTime = SnodeAPI.nowWithOffset + if (ConfigBase.isNewConfigEnabled(forcedConfig, currentTime)) { + scheduleConfigSync(userPublicKey) + return + } val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context) val now = System.currentTimeMillis() if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return @@ -35,7 +83,16 @@ object ConfigurationMessageUtilities { } fun forceSyncConfigurationNowIfNeeded(context: Context): Promise { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit) + // add if check here to schedule new config job process and return early + val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofFail(NullPointerException("User Public Key is null")) + val forcedConfig = TextSecurePreferences.hasForcedNewConfig(context) + val currentTime = SnodeAPI.nowWithOffset + if (ConfigBase.isNewConfigEnabled(forcedConfig, currentTime)) { + // schedule job if none exist + // don't schedule job if we already have one + scheduleConfigSync(userPublicKey) + return Promise.ofSuccess(Unit) + } val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> !recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() }.map { recipient -> @@ -50,9 +107,179 @@ object ConfigurationMessageUtilities { ) } val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit) - val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) + val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)), isSyncMessage = true) TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) return promise } + private fun maybeUserSecretKey() = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes + + fun generateUserProfileConfigDump(): ByteArray? { + val storage = MessagingModuleConfiguration.shared.storage + val ownPublicKey = storage.getUserPublicKey() ?: return null + val config = ConfigurationMessage.getCurrent(listOf()) ?: return null + val secretKey = maybeUserSecretKey() ?: return null + val profile = UserProfile.newInstance(secretKey) + profile.setName(config.displayName) + val picUrl = config.profilePicture + val picKey = config.profileKey + if (!picUrl.isNullOrEmpty() && picKey.isNotEmpty()) { + profile.setPic(UserPic(picUrl, picKey)) + } + val ownThreadId = storage.getThreadId(Address.fromSerialized(ownPublicKey)) + profile.setNtsPriority( + if (ownThreadId != null) + if (storage.isPinned(ownThreadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE + else ConfigBase.PRIORITY_HIDDEN + ) + val dump = profile.dump() + profile.free() + return dump + } + + fun generateContactConfigDump(): ByteArray? { + val secretKey = maybeUserSecretKey() ?: return null + val storage = MessagingModuleConfiguration.shared.storage + val localUserKey = storage.getUserPublicKey() ?: return null + val contactsWithSettings = storage.getAllContacts().filter { recipient -> + recipient.sessionID != localUserKey && recipient.sessionID.startsWith(IdPrefix.STANDARD.value) + && storage.getThreadId(recipient.sessionID) != null + }.map { contact -> + val address = Address.fromSerialized(contact.sessionID) + val thread = storage.getThreadId(address) + val isPinned = if (thread != null) { + storage.isPinned(thread) + } else false + + Triple(contact, storage.getRecipientSettings(address)!!, isPinned) + } + val contactConfig = Contacts.newInstance(secretKey) + for ((contact, settings, isPinned) in contactsWithSettings) { + val url = contact.profilePictureURL + val key = contact.profilePictureEncryptionKey + val userPic = if (url.isNullOrEmpty() || key?.isNotEmpty() != true) { + null + } else { + UserPic(url, key) + } + + val contactInfo = Contact( + id = contact.sessionID, + name = contact.name.orEmpty(), + nickname = contact.nickname.orEmpty(), + blocked = settings.isBlocked, + approved = settings.isApproved, + approvedMe = settings.hasApprovedMe(), + profilePicture = userPic ?: UserPic.DEFAULT, + priority = if (isPinned) 1 else 0, + expiryMode = if (settings.expireMessages == 0) ExpiryMode.NONE else ExpiryMode.AfterRead(settings.expireMessages.toLong()) + ) + contactConfig.set(contactInfo) + } + val dump = contactConfig.dump() + contactConfig.free() + if (dump.isEmpty()) return null + return dump + } + + fun generateConversationVolatileDump(context: Context): ByteArray? { + val secretKey = maybeUserSecretKey() ?: return null + val storage = MessagingModuleConfiguration.shared.storage + val convoConfig = ConversationVolatileConfig.newInstance(secretKey) + val threadDb = DatabaseComponent.get(context).threadDatabase() + threadDb.approvedConversationList.use { cursor -> + val reader = threadDb.readerFor(cursor) + var current = reader.next + while (current != null) { + val recipient = current.recipient + val contact = when { + recipient.isOpenGroupRecipient -> { + val openGroup = storage.getOpenGroup(current.threadId) ?: continue + val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue + convoConfig.getOrConstructCommunity(base, room, pubKey) + } + recipient.isClosedGroupRecipient -> { + val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) + convoConfig.getOrConstructLegacyGroup(groupPublicKey) + } + recipient.isContactRecipient -> { + if (recipient.isLocalNumber) null // this is handled by the user profile NTS data + else if (recipient.isOpenGroupInboxRecipient) null // specifically exclude + else if (!recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) null + else convoConfig.getOrConstructOneToOne(recipient.address.serialize()) + } + else -> null + } + if (contact == null) { + current = reader.next + continue + } + contact.lastRead = current.lastSeen + contact.unread = false + convoConfig.set(contact) + current = reader.next + } + } + + val dump = convoConfig.dump() + convoConfig.free() + if (dump.isEmpty()) return null + return dump + } + + fun generateUserGroupDump(context: Context): ByteArray? { + val secretKey = maybeUserSecretKey() ?: return null + val storage = MessagingModuleConfiguration.shared.storage + val groupConfig = UserGroupsConfig.newInstance(secretKey) + val allOpenGroups = storage.getAllOpenGroups().values.mapNotNull { openGroup -> + val (baseUrl, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@mapNotNull null + val pubKeyHex = Hex.toStringCondensed(pubKey) + val baseInfo = BaseCommunityInfo(baseUrl, room, pubKeyHex) + val threadId = storage.getThreadId(openGroup) ?: return@mapNotNull null + val isPinned = storage.isPinned(threadId) + GroupInfo.CommunityGroupInfo(baseInfo, if (isPinned) 1 else 0) + } + + val allLgc = storage.getAllGroups(includeInactive = false).filter { + it.isClosedGroup && it.isActive && it.members.size > 1 + }.mapNotNull { group -> + val groupAddress = Address.fromSerialized(group.encodedId) + val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupAddress.serialize()).toHexString() + val recipient = storage.getRecipientSettings(groupAddress) ?: return@mapNotNull null + val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return@mapNotNull null + val threadId = storage.getThreadId(group.encodedId) + val isPinned = threadId?.let { storage.isPinned(threadId) } ?: false + val admins = group.admins.map { it.serialize() to true }.toMap() + val members = group.members.filterNot { it.serialize() !in admins.keys }.map { it.serialize() to false }.toMap() + GroupInfo.LegacyGroupInfo( + sessionId = groupPublicKey, + name = group.title, + members = admins + members, + priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, + encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte + encSecKey = encryptionKeyPair.privateKey.serialize(), + disappearingTimer = recipient.expireMessages.toLong(), + joinedAt = (group.formationTimestamp / 1000L) + ) + } + (allOpenGroups + allLgc).forEach { groupInfo -> + groupConfig.set(groupInfo) + } + val dump = groupConfig.dump() + groupConfig.free() + if (dump.isEmpty()) return null + return dump + } + + @JvmField + val DELETE_INACTIVE_GROUPS: String = """ + DELETE FROM ${GroupDatabase.TABLE_NAME} WHERE ${GroupDatabase.GROUP_ID} IN (SELECT ${ThreadDatabase.ADDRESS} FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%'); + DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.ADDRESS} IN (SELECT ${ThreadDatabase.ADDRESS} FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%'); + """.trimIndent() + + @JvmField + val DELETE_INACTIVE_ONE_TO_ONES: String = """ + DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_INBOX_PREFIX}%'; + """.trimIndent() + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ContextProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ContextProvider.kt deleted file mode 100644 index 4bc8e104d..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ContextProvider.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.thoughtcrime.securesms.util - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.fragment.app.Fragment - -/** - * A simplified version of [android.content.ContextWrapper], - * but properly supports [startActivityForResult] for the implementations. - */ -interface ContextProvider { - fun getContext(): Context - fun startActivityForResult(intent: Intent, requestCode: Int) -} - -class ActivityContextProvider(private val activity: Activity): ContextProvider { - - override fun getContext(): Context { - return activity - } - - override fun startActivityForResult(intent: Intent, requestCode: Int) { - activity.startActivityForResult(intent, requestCode) - } -} - -class FragmentContextProvider(private val fragment: Fragment): ContextProvider { - - override fun getContext(): Context { - return fragment.requireContext() - } - - override fun startActivityForResult(intent: Intent, requestCode: Int) { - fragment.startActivityForResult(intent, requestCode) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt new file mode 100644 index 000000000..9cff8c77b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt @@ -0,0 +1,6 @@ +package org.thoughtcrime.securesms.util + +import android.database.Cursor + +fun Cursor.asSequence(): Sequence = + generateSequence { if (moveToNext()) this else null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java index 874440f5d..66c838cc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -67,7 +67,8 @@ public class DateUtils extends android.text.format.DateUtils { } public static String getDisplayFormattedTimeSpanString(final Context c, final Locale locale, final long timestamp) { - if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { + // If the timestamp is invalid (ie. 0) then assume we're waiting on data and just use the 'Now' copy + if (timestamp == 0 || isWithin(timestamp, 1, TimeUnit.MINUTES)) { return c.getString(R.string.DateUtils_just_now); } else if (isToday(timestamp)) { return getFormattedDateTime(timestamp, getHourFormat(c), locale); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt index 00e3e4441..a38c93831 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt @@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.util import android.content.res.Resources import android.os.Build import androidx.annotation.ColorRes +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.max import kotlin.math.roundToInt fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int { @@ -30,3 +32,8 @@ fun toDp(px: Float, resources: Resources): Float { val scale = resources.displayMetrics.density return (px / scale) } + +val RecyclerView.isScrolledToBottom: Boolean + get() = computeVerticalScrollOffset().coerceAtLeast(0) + + computeVerticalScrollExtent() + + toPx(50, resources) >= computeVerticalScrollRange() diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt b/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt index 08b81e5cb..c7d53c1fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt @@ -7,6 +7,7 @@ import android.graphics.Canvas import android.graphics.Paint import android.util.AttributeSet import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.annotation.ColorInt @@ -55,16 +56,21 @@ object GlowViewUtilities { animation.start() } - fun animateShadowColorChange(view: GlowView, @ColorInt startColor: Int, @ColorInt endColor: Int) { + fun animateShadowColorChange( + view: GlowView, + @ColorInt startColor: Int, + @ColorInt endColor: Int, + duration: Long = 250 + ) { val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor) - animation.duration = 250 + animation.duration = duration + animation.interpolator = AccelerateDecelerateInterpolator() animation.addUpdateListener { animator -> val color = animator.animatedValue as Int view.sessionShadowColor = color } animation.start() } - } class PNModeView : LinearLayout, GlowView { @@ -223,3 +229,59 @@ class InputBarButtonImageViewContainer : RelativeLayout, GlowView { } // endregion } + +class MessageBubbleView : androidx.constraintlayout.widget.ConstraintLayout, GlowView { + @ColorInt override var mainColor: Int = 0 + set(newValue) { field = newValue; paint.color = newValue } + @ColorInt override var sessionShadowColor: Int = 0 + set(newValue) { + field = newValue + shadowPaint.setShadowLayer(toPx(10, resources).toFloat(), 0.0f, 0.0f, newValue) + + if (numShadowRenders == 0) { + numShadowRenders = 1 + } + + invalidate() + } + var cornerRadius: Float = 0f + var numShadowRenders: Int = 0 + + private val paint: Paint by lazy { + val result = Paint() + result.style = Paint.Style.FILL + result.isAntiAlias = true + result + } + + private val shadowPaint: Paint by lazy { + val result = Paint() + result.style = Paint.Style.FILL + result.isAntiAlias = true + result + } + + // region Lifecycle + constructor(context: Context) : super(context) { } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { } + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { } + + init { + setWillNotDraw(false) + } + // endregion + + // region Updating + override fun onDraw(c: Canvas) { + val w = width.toFloat() + val h = height.toFloat() + + (0 until numShadowRenders).forEach { + c.drawRoundRect(0f, 0f, w, h, cornerRadius, cornerRadius, shadowPaint) + } + + c.drawRoundRect(0f, 0f, w, h, cornerRadius, cornerRadius, paint) + super.onDraw(c) + } + // endregion +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java b/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java deleted file mode 100644 index 82077f474..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Color; -import androidx.core.content.ContextCompat; -import android.text.Layout; -import android.text.Selection; -import android.text.Spannable; -import android.text.method.LinkMovementMethod; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.View; -import android.widget.TextView; - -import network.loki.messenger.R; - -public class LongClickMovementMethod extends LinkMovementMethod { - @SuppressLint("StaticFieldLeak") - private static LongClickMovementMethod sInstance; - - private final GestureDetector gestureDetector; - private View widget; - private LongClickCopySpan currentSpan; - - private LongClickMovementMethod(final Context context) { - gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { - @Override - public void onLongPress(MotionEvent e) { - if (currentSpan != null && widget != null) { - currentSpan.onLongClick(widget); - widget = null; - currentSpan = null; - } - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - if (currentSpan != null && widget != null) { - currentSpan.onClick(widget); - widget = null; - currentSpan = null; - } - return true; - } - }); - } - - @Override - public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { - int action = event.getAction(); - - if (action == MotionEvent.ACTION_UP || - action == MotionEvent.ACTION_DOWN) { - int x = (int) event.getX(); - int y = (int) event.getY(); - - x -= widget.getTotalPaddingLeft(); - y -= widget.getTotalPaddingTop(); - - x += widget.getScrollX(); - y += widget.getScrollY(); - - Layout layout = widget.getLayout(); - int line = layout.getLineForVertical(y); - int off = layout.getOffsetForHorizontal(line, x); - - LongClickCopySpan longClickCopySpan[] = buffer.getSpans(off, off, LongClickCopySpan.class); - if (longClickCopySpan.length != 0) { - LongClickCopySpan aSingleSpan = longClickCopySpan[0]; - if (action == MotionEvent.ACTION_DOWN) { - Selection.setSelection(buffer, buffer.getSpanStart(aSingleSpan), - buffer.getSpanEnd(aSingleSpan)); - aSingleSpan.setHighlighted(true, - ContextCompat.getColor(widget.getContext(), R.color.touch_highlight)); - } else { - Selection.removeSelection(buffer); - aSingleSpan.setHighlighted(false, Color.TRANSPARENT); - } - - this.currentSpan = aSingleSpan; - this.widget = widget; - return gestureDetector.onTouchEvent(event); - } - } else if (action == MotionEvent.ACTION_CANCEL) { - // Remove Selections. - LongClickCopySpan[] spans = buffer.getSpans(Selection.getSelectionStart(buffer), - Selection.getSelectionEnd(buffer), LongClickCopySpan.class); - for (LongClickCopySpan aSpan : spans) { - aSpan.setHighlighted(false, Color.TRANSPARENT); - } - Selection.removeSelection(buffer); - return gestureDetector.onTouchEvent(event); - } - return super.onTouchEvent(widget, buffer, event); - } - - public static LongClickMovementMethod getInstance(Context context) { - if (sInstance == null) { - sInstance = new LongClickMovementMethod(context.getApplicationContext()); - } - return sInstance; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt index 10d507a53..06fda2930 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt @@ -7,8 +7,6 @@ import org.session.libsession.messaging.messages.signal.IncomingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI -import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.recipients.Recipient @@ -21,7 +19,6 @@ import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.groups.GroupManager import java.security.SecureRandom -import java.util.* import kotlin.random.asKotlinRandom object MockDataGenerator { @@ -139,7 +136,6 @@ object MockDataGenerator { false ), (timestampNow - (index * 5000)), - false, false ) } @@ -235,8 +231,9 @@ object MockDataGenerator { // Add the group to the user's set of public keys to poll for and store the key pair val encryptionKeyPair = Curve.generateKeyPair() - storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, randomGroupPublicKey) + storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, randomGroupPublicKey, System.currentTimeMillis()) storage.setExpirationTimer(groupId, 0) + storage.createInitialConfigGroup(randomGroupPublicKey, groupName, GroupUtil.createConfigMemberMap(members, setOf(adminUserId)), System.currentTimeMillis(), encryptionKeyPair) // Add the group created message if (userSessionId == adminUserId) { @@ -269,7 +266,6 @@ object MockDataGenerator { false ), (timestampNow - (index * 5000)), - false, false ) } @@ -395,7 +391,6 @@ object MockDataGenerator { false ), (timestampNow - (index * 5000)), - false, false ) } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt index 59658f12a..8b219849a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.util import android.content.ContentResolver import android.content.ContentValues import android.content.Context -import android.content.DialogInterface.OnClickListener import android.media.MediaScannerConnection import android.net.Uri import android.os.Build @@ -12,12 +11,12 @@ import android.provider.MediaStore import android.text.TextUtils import android.webkit.MimeTypeMap import android.widget.Toast -import androidx.appcompat.app.AlertDialog import network.loki.messenger.R import org.session.libsession.utilities.task.ProgressDialogAsyncTask import org.session.libsignal.utilities.ExternalStorageUtil import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.showSessionDialog import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -30,7 +29,12 @@ import java.util.concurrent.TimeUnit * Saves attachment files to an external storage using [MediaStore] API. * Requires [android.Manifest.permission.WRITE_EXTERNAL_STORAGE] on API 28 and below. */ -class SaveAttachmentTask : ProgressDialogAsyncTask> { +class SaveAttachmentTask @JvmOverloads constructor(context: Context, count: Int = 1) : + ProgressDialogAsyncTask>( + context, + context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count), + context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count) + ) { companion object { @JvmStatic @@ -41,30 +45,25 @@ class SaveAttachmentTask : ProgressDialogAsyncTask Unit = {}) { + context.showSessionDialog { + title(R.string.ConversationFragment_save_to_sd_card) + iconAttribute(R.attr.dialog_alert_icon) + text(context.resources.getQuantityString( R.plurals.ConversationFragment_saving_n_media_to_storage_warning, count, count)) - builder.setPositiveButton(R.string.yes, onAcceptListener) - builder.setNegativeButton(R.string.no, null) - builder.show() + button(R.string.yes) { onAcceptListener() } + button(R.string.no) + } } } private val contextReference: WeakReference - private val attachmentCount: Int + private val attachmentCount: Int = count - @JvmOverloads - constructor(context: Context, count: Int = 1): super(context, - context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count), - context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count)) { + init { this.contextReference = WeakReference(context) - this.attachmentCount = count } override fun doInBackground(vararg attachments: Attachment?): Pair { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt index 05b6fe86f..c10e1b635 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt @@ -49,11 +49,11 @@ object SessionMetaProtocol { @JvmStatic fun shouldSendReadReceipt(recipient: Recipient): Boolean { - return !recipient.isGroupRecipient && recipient.isApproved + return !recipient.isGroupRecipient && recipient.isApproved && !recipient.isBlocked } @JvmStatic fun shouldSendTypingIndicator(recipient: Recipient): Boolean { - return !recipient.isGroupRecipient && recipient.isApproved + return !recipient.isGroupRecipient && recipient.isApproved && !recipient.isBlocked } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt new file mode 100644 index 000000000..b15d82a33 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.util + +import network.loki.messenger.libsession_util.ConversationVolatileConfig +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.utilities.GroupUtil +import org.session.libsignal.utilities.IdPrefix +import org.thoughtcrime.securesms.database.model.ThreadRecord + +fun ConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Boolean { + val recipient = thread.recipient + if (recipient.isContactRecipient + && recipient.isOpenGroupInboxRecipient + && recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) { + return getOneToOne(recipient.address.serialize())?.unread == true + } else if (recipient.isClosedGroupRecipient) { + return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true + } else if (recipient.isOpenGroupRecipient) { + val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false + return getCommunity(openGroup.server, openGroup.room)?.unread == true + } + return false +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SimpleTextWatcher.java b/app/src/main/java/org/thoughtcrime/securesms/util/SimpleTextWatcher.java similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/contactshare/SimpleTextWatcher.java rename to app/src/main/java/org/thoughtcrime/securesms/util/SimpleTextWatcher.java index b2448b8f8..512748bae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SimpleTextWatcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SimpleTextWatcher.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.contactshare; +package org.thoughtcrime.securesms.util; import android.text.Editable; import android.text.TextWatcher; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt index 7b7f3a04f..dfd4ffe41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt @@ -5,14 +5,18 @@ import android.animation.AnimatorListenerAdapter import android.animation.FloatEvaluator import android.animation.ValueAnimator import android.content.Context +import android.graphics.Bitmap import android.graphics.PointF import android.graphics.Rect +import android.util.Size import android.view.View import androidx.annotation.ColorInt import androidx.annotation.DimenRes import network.loki.messenger.R import org.session.libsession.utilities.getColorFromAttr import android.view.inputmethod.InputMethodManager +import androidx.core.graphics.applyCanvas +import kotlin.math.roundToInt fun View.contains(point: PointF): Boolean { return hitRect.contains(point.x.toInt(), point.y.toInt()) @@ -54,7 +58,7 @@ fun View.fadeIn(duration: Long = 150) { fun View.fadeOut(duration: Long = 150) { animate().setDuration(duration).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) visibility = View.GONE } @@ -65,3 +69,24 @@ fun View.hideKeyboard() { val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(this.windowToken, 0) } + + +fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap { + val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth) + val scale = size.width / measuredWidth.toFloat() + + return Bitmap.createBitmap(size.width, size.height, config).applyCanvas { + scale(scale, scale) + translate(-scrollX.toFloat(), -scrollY.toFloat()) + draw(this) + } +} + +fun Size.coerceAtMost(longestWidth: Int): Size = + (width.toFloat() / height).let { aspect -> + if (aspect > 1) { + width.coerceAtMost(longestWidth).let { Size(it, (it / aspect).roundToInt()) } + } else { + height.coerceAtMost(longestWidth).let { Size((it * aspect).roundToInt(), it) } + } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java deleted file mode 100644 index bba19f1fc..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.content.Context; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import androidx.annotation.NonNull; - -import org.session.libsession.utilities.ServiceUtil; -import org.session.libsignal.utilities.Log; - -public class WakeLockUtil { - - private static final String TAG = WakeLockUtil.class.getSimpleName(); - - /** - * @param tag will be prefixed with "signal:" if it does not already start with it. - */ - public static WakeLock acquire(@NonNull Context context, int lockType, long timeout, @NonNull String tag) { - tag = prefixTag(tag); - try { - PowerManager powerManager = ServiceUtil.getPowerManager(context); - WakeLock wakeLock = powerManager.newWakeLock(lockType, tag); - - wakeLock.acquire(timeout); - Log.d(TAG, "Acquired wakelock with tag: " + tag); - - return wakeLock; - } catch (Exception e) { - Log.w(TAG, "Failed to acquire wakelock with tag: " + tag, e); - return null; - } - } - - /** - * @param tag will be prefixed with "signal:" if it does not already start with it. - */ - public static void release(@NonNull WakeLock wakeLock, @NonNull String tag) { - tag = prefixTag(tag); - try { - if (wakeLock.isHeld()) { - wakeLock.release(); - Log.d(TAG, "Released wakelock with tag: " + tag); - } else { - Log.d(TAG, "Wakelock wasn't held at time of release: " + tag); - } - } catch (Exception e) { - Log.w(TAG, "Failed to release wakelock with tag: " + tag, e); - } - } - - private static String prefixTag(@NonNull String tag) { - return tag.startsWith("signal:") ? tag : "signal:" + tag; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SelectableItem.kt b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SelectableItem.kt new file mode 100644 index 000000000..88b41d11c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SelectableItem.kt @@ -0,0 +1,3 @@ +package org.thoughtcrime.securesms.util.adapter + +data class SelectableItem(val item: T, val isSelected: Boolean) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioEvent.kt deleted file mode 100644 index b01edfb49..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.thoughtcrime.securesms.webrtc - -enum class AudioEvent { - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index b7a9b6fd6..894de9de6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -18,6 +18,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.messages.control.CallMessage 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.Debouncer import org.session.libsession.utilities.Util @@ -92,6 +93,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va peerConnectionObservers.remove(listener) } + fun shutDownAudioManager() { + signalAudioManager.shutdown() + } + private val _audioEvents = MutableStateFlow(AudioEnabled(false)) val audioEvents = _audioEvents.asSharedFlow() private val _videoEvents = MutableStateFlow(VideoEnabled(false)) @@ -298,7 +303,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va sdpMLineIndexes = sdpMLineIndexes, sdpMids = sdpMids, currentCallId - ), currentRecipient.address) + ), currentRecipient.address, isSyncMessage = currentRecipient.isLocalNumber) } } } @@ -432,7 +437,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va pendingIncomingIceUpdates.clear() val answerMessage = CallMessage.answer(answer.description, callId) Log.i("Loki", "Posting new answer") - MessageSender.sendNonDurably(answerMessage, recipient.address) + MessageSender.sendNonDurably(answerMessage, recipient.address, isSyncMessage = recipient.isLocalNumber) } else { Promise.ofFail(Exception("Couldn't reconnect from current state")) } @@ -476,11 +481,11 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va connection.setLocalDescription(answer) val answerMessage = CallMessage.answer(answer.description, callId) val userAddress = storage.getUserPublicKey() ?: return Promise.ofFail(NullPointerException("No user public key")) - MessageSender.sendNonDurably(answerMessage, Address.fromSerialized(userAddress)) + MessageSender.sendNonDurably(answerMessage, Address.fromSerialized(userAddress), isSyncMessage = true) val sendAnswerMessage = MessageSender.sendNonDurably(CallMessage.answer( answer.description, callId - ), recipient.address) + ), recipient.address, isSyncMessage = recipient.isLocalNumber) insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_INCOMING, false) @@ -530,13 +535,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va Log.d("Loki", "Sending pre-offer") return MessageSender.sendNonDurably(CallMessage.preOffer( callId - ), recipient.address).bind { + ), recipient.address, isSyncMessage = recipient.isLocalNumber).bind { Log.d("Loki", "Sent pre-offer") Log.d("Loki", "Sending offer") MessageSender.sendNonDurably(CallMessage.offer( offer.description, callId - ), recipient.address).success { + ), recipient.address, isSyncMessage = recipient.isLocalNumber).success { Log.d("Loki", "Sent offer") }.fail { Log.e("Loki", "Failed to send offer", it) @@ -550,8 +555,8 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va val recipient = recipient ?: return val userAddress = storage.getUserPublicKey() ?: return stateProcessor.processEvent(Event.DeclineCall) { - MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress)) - MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address) + MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress), isSyncMessage = true) + MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address, isSyncMessage = recipient.isLocalNumber) insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_MISSED) } } @@ -570,11 +575,11 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va val buffer = DataChannel.Buffer(ByteBuffer.wrap(HANGUP_JSON.toString().encodeToByteArray()), false) channel.send(buffer) } - MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address) + MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address, isSyncMessage = recipient.isLocalNumber) } } - fun insertCallMessage(threadPublicKey: String, callMessageType: CallMessageType, signal: Boolean = false, sentTimestamp: Long = System.currentTimeMillis()) { + fun insertCallMessage(threadPublicKey: String, callMessageType: CallMessageType, signal: Boolean = false, sentTimestamp: Long = SnodeAPI.nowWithOffset) { storage.insertCallMessage(threadPublicKey, callMessageType, sentTimestamp) } @@ -721,7 +726,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va }) connection.setLocalDescription(offer) - MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId), recipient.address) + MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId), recipient.address, isSyncMessage = recipient.isLocalNumber) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt index bb41c7c97..3d40b5f74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.webrtc import android.app.NotificationManager import android.content.Context +import android.content.Intent import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope @@ -32,6 +33,20 @@ class CallMessageProcessor(private val context: Context, private val textSecureP companion object { private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L + + fun safeStartService(context: Context, intent: Intent) { + // If the foreground service crashes then it's possible for one of these intents to + // be started in the background (in which case 'startService' will throw a + // 'BackgroundServiceStartNotAllowedException' exception) so catch that case and try + // to re-start the service in the foreground + try { context.startService(intent) } + catch(e: Exception) { + try { ContextCompat.startForegroundService(context, intent) } + catch (e2: Exception) { + Log.e("Loki", "Unable to start CallMessage intent: ${e2.message}") + } + } + } } init { @@ -90,7 +105,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP private fun incomingHangup(callMessage: CallMessage) { val callId = callMessage.callId ?: return val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId) - context.startService(hangupIntent) + safeStartService(context, hangupIntent) } private fun incomingAnswer(callMessage: CallMessage) { @@ -103,7 +118,8 @@ class CallMessageProcessor(private val context: Context, private val textSecureP sdp = sdp, callId = callId ) - context.startService(answerIntent) + + safeStartService(context, answerIntent) } private fun handleIceCandidates(callMessage: CallMessage) { @@ -119,7 +135,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP callId = callId, address = Address.fromSerialized(sender) ) - context.startService(iceIntent) + safeStartService(context, iceIntent) } private fun incomingPreOffer(callMessage: CallMessage) { @@ -132,7 +148,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP callId = callId, callTime = callMessage.sentTimestamp!! ) - context.startService(incomingIntent) + safeStartService(context, incomingIntent) } private fun incomingCall(callMessage: CallMessage) { @@ -146,7 +162,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP callId = callId, callTime = callMessage.sentTimestamp!! ) - context.startService(incomingIntent) + safeStartService(context, incomingIntent) } private fun CallMessage.iceCandidates(): List { diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioHandler.kt index 89eba2a3a..7ca44f46d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioHandler.kt @@ -15,7 +15,7 @@ class SignalAudioHandler(looper: Looper) : Handler(looper) { } } - fun isOnHandler(): Boolean { + private fun isOnHandler(): Boolean { return Looper.myLooper() == looper } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt index 67514c58b..229cbd13d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt @@ -9,6 +9,7 @@ import android.media.SoundPool import android.os.HandlerThread import network.loki.messenger.R import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.webrtc.AudioManagerCommand import org.thoughtcrime.securesms.webrtc.audio.SignalBluetoothManager.State as BState @@ -32,10 +33,10 @@ class SignalAudioManager(private val context: Context, private val eventListener: EventListener?, private val androidAudioManager: AudioManagerCompat) { - private var commandAndControlThread: HandlerThread? = HandlerThread("call-audio").apply { start() } - private var handler: SignalAudioHandler? = null + private var commandAndControlThread: HandlerThread? = HandlerThread("call-audio", ThreadUtils.PRIORITY_IMPORTANT_BACKGROUND_THREAD).apply { start() } + private var handler: SignalAudioHandler = SignalAudioHandler(commandAndControlThread!!.looper) - private var signalBluetoothManager: SignalBluetoothManager? = null + private var signalBluetoothManager: SignalBluetoothManager = SignalBluetoothManager(context, this, androidAudioManager, handler) private var state: State = State.UNINITIALIZED @@ -62,12 +63,9 @@ class SignalAudioManager(private val context: Context, private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null fun handleCommand(command: AudioManagerCommand) { - if (command == AudioManagerCommand.Initialize) { - initialize() - return - } - handler?.post { + handler.post { when (command) { + is AudioManagerCommand.Initialize -> initialize() is AudioManagerCommand.UpdateAudioDeviceState -> updateAudioDeviceState() is AudioManagerCommand.Start -> start() is AudioManagerCommand.Stop -> stop(command.playDisconnect) @@ -84,34 +82,37 @@ class SignalAudioManager(private val context: Context, Log.i(TAG, "Initializing audio manager state: $state") if (state == State.UNINITIALIZED) { - commandAndControlThread = HandlerThread("call-audio").apply { start() } - handler = SignalAudioHandler(commandAndControlThread!!.looper) + savedAudioMode = androidAudioManager.mode + savedIsSpeakerPhoneOn = androidAudioManager.isSpeakerphoneOn + savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute + hasWiredHeadset = androidAudioManager.isWiredHeadsetOn - signalBluetoothManager = SignalBluetoothManager(context, this, androidAudioManager, handler!!) + androidAudioManager.requestCallAudioFocus() - handler!!.post { + setMicrophoneMute(false) - savedAudioMode = androidAudioManager.mode - savedIsSpeakerPhoneOn = androidAudioManager.isSpeakerphoneOn - savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute - hasWiredHeadset = androidAudioManager.isWiredHeadsetOn + audioDevices.clear() - androidAudioManager.requestCallAudioFocus() + signalBluetoothManager.start() - setMicrophoneMute(false) + updateAudioDeviceState() - audioDevices.clear() + wiredHeadsetReceiver = WiredHeadsetReceiver() + context.registerReceiver(wiredHeadsetReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG)) - signalBluetoothManager!!.start() + state = State.PREINITIALIZED - updateAudioDeviceState() + Log.d(TAG, "Initialized") + } + } - wiredHeadsetReceiver = WiredHeadsetReceiver() - context.registerReceiver(wiredHeadsetReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG)) - - state = State.PREINITIALIZED - - Log.d(TAG, "Initialized") + fun shutdown() { + handler.post { + stop(false) + if (commandAndControlThread != null) { + Log.i(TAG, "Shutting down command and control") + commandAndControlThread?.quitSafely() + commandAndControlThread = null } } } @@ -138,23 +139,11 @@ class SignalAudioManager(private val context: Context, private fun stop(playDisconnect: Boolean) { Log.d(TAG, "Stopping. state: $state") - if (state == State.UNINITIALIZED) { - Log.i(TAG, "Trying to stop AudioManager in incorrect state: $state") - return - } - handler?.post { - incomingRinger.stop() - outgoingRinger.stop() - stop(false) - if (commandAndControlThread != null) { - Log.i(TAG, "Shutting down command and control") - commandAndControlThread?.quitSafely() - commandAndControlThread = null - } - } + incomingRinger.stop() + outgoingRinger.stop() - if (playDisconnect) { + if (playDisconnect && state != State.UNINITIALIZED) { val volume: Float = androidAudioManager.ringVolumeWithMinimum() soundPool.play(disconnectedSoundId, volume, volume, 0, 0, 1.0f) } @@ -170,7 +159,7 @@ class SignalAudioManager(private val context: Context, } wiredHeadsetReceiver = null - signalBluetoothManager?.stop() + signalBluetoothManager.stop() setSpeakerphoneOn(savedIsSpeakerPhoneOn) setMicrophoneMute(savedIsMicrophoneMute) @@ -183,25 +172,25 @@ class SignalAudioManager(private val context: Context, } private fun updateAudioDeviceState() { - handler!!.assertHandlerThread() + handler.assertHandlerThread() Log.i( TAG, "updateAudioDeviceState(): " + "wired: $hasWiredHeadset " + - "bt: ${signalBluetoothManager!!.state} " + + "bt: ${signalBluetoothManager.state} " + "available: $audioDevices " + "selected: $selectedAudioDevice " + "userSelected: $userSelectedAudioDevice" ) - if (signalBluetoothManager!!.state.shouldUpdate()) { - signalBluetoothManager!!.updateDevice() + if (signalBluetoothManager.state.shouldUpdate()) { + signalBluetoothManager.updateDevice() } val newAudioDevices = mutableSetOf(AudioDevice.SPEAKER_PHONE) - if (signalBluetoothManager!!.state.hasDevice()) { + if (signalBluetoothManager.state.hasDevice()) { newAudioDevices += AudioDevice.BLUETOOTH } @@ -217,7 +206,7 @@ class SignalAudioManager(private val context: Context, var audioDeviceSetUpdated = audioDevices != newAudioDevices audioDevices = newAudioDevices - if (signalBluetoothManager!!.state == BState.UNAVAILABLE && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { + if (signalBluetoothManager.state == BState.UNAVAILABLE && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { userSelectedAudioDevice = AudioDevice.NONE } @@ -230,7 +219,7 @@ class SignalAudioManager(private val context: Context, userSelectedAudioDevice = AudioDevice.NONE } - val btState = signalBluetoothManager!!.state + val btState = signalBluetoothManager.state val needBluetoothAudioStart = btState == BState.AVAILABLE && (userSelectedAudioDevice == AudioDevice.NONE || userSelectedAudioDevice == AudioDevice.BLUETOOTH || autoSwitchToBluetooth) @@ -238,27 +227,27 @@ class SignalAudioManager(private val context: Context, (userSelectedAudioDevice != AudioDevice.NONE && userSelectedAudioDevice != AudioDevice.BLUETOOTH) if (btState.hasDevice()) { - Log.i(TAG, "Need bluetooth audio: state: ${signalBluetoothManager!!.state} start: $needBluetoothAudioStart stop: $needBluetoothAudioStop") + Log.i(TAG, "Need bluetooth audio: state: ${signalBluetoothManager.state} start: $needBluetoothAudioStart stop: $needBluetoothAudioStop") } if (needBluetoothAudioStop) { - signalBluetoothManager!!.stopScoAudio() - signalBluetoothManager!!.updateDevice() + signalBluetoothManager.stopScoAudio() + signalBluetoothManager.updateDevice() } - if (!autoSwitchToBluetooth && signalBluetoothManager!!.state == BState.UNAVAILABLE) { + if (!autoSwitchToBluetooth && signalBluetoothManager.state == BState.UNAVAILABLE) { autoSwitchToBluetooth = true } - if (needBluetoothAudioStart && !needBluetoothAudioStop) { - if (!signalBluetoothManager!!.startScoAudio()) { + if (!needBluetoothAudioStop && needBluetoothAudioStart) { + if (!signalBluetoothManager.startScoAudio()) { Log.e(TAG,"Failed to start sco audio") audioDevices.remove(AudioDevice.BLUETOOTH) audioDeviceSetUpdated = true } } - if (autoSwitchToBluetooth && signalBluetoothManager!!.state == BState.CONNECTED) { + if (autoSwitchToBluetooth && signalBluetoothManager.state == BState.CONNECTED) { userSelectedAudioDevice = AudioDevice.BLUETOOTH autoSwitchToBluetooth = false } @@ -373,7 +362,7 @@ class SignalAudioManager(private val context: Context, val pluggedIn = intent.getIntExtra("state", 0) == 1 val hasMic = intent.getIntExtra("microphone", 0) == 1 - handler?.post { onWiredHeadsetChange(pluggedIn, hasMic) } + handler.post { onWiredHeadsetChange(pluggedIn, hasMic) } } } diff --git a/app/src/main/res/color/button_destructive.xml b/app/src/main/res/color/button_destructive.xml new file mode 100644 index 000000000..cefbfed23 --- /dev/null +++ b/app/src/main/res/color/button_destructive.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/prominent_button_color.xml b/app/src/main/res/color/prominent_button_color.xml new file mode 100644 index 000000000..39985565d --- /dev/null +++ b/app/src/main/res/color/prominent_button_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/borderless_button_medium_background.xml b/app/src/main/res/drawable/borderless_button_medium_background.xml new file mode 100644 index 000000000..6c72f9e72 --- /dev/null +++ b/app/src/main/res/drawable/borderless_button_medium_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/conversation_pinned_background.xml b/app/src/main/res/drawable/conversation_pinned_background.xml index 104b9c272..eb64dc7f5 100644 --- a/app/src/main/res/drawable/conversation_pinned_background.xml +++ b/app/src/main/res/drawable/conversation_pinned_background.xml @@ -1,7 +1,7 @@ + android:color="?android:colorControlHighlight"> diff --git a/app/src/main/res/drawable/conversation_unread_background.xml b/app/src/main/res/drawable/conversation_unread_background.xml index de0f5fb68..9e9bb9436 100644 --- a/app/src/main/res/drawable/conversation_unread_background.xml +++ b/app/src/main/res/drawable/conversation_unread_background.xml @@ -1,7 +1,7 @@ + android:color="?android:colorControlHighlight"> diff --git a/app/src/main/res/drawable/conversation_view_background.xml b/app/src/main/res/drawable/conversation_view_background.xml index aaceb7ed5..2f177318e 100644 --- a/app/src/main/res/drawable/conversation_view_background.xml +++ b/app/src/main/res/drawable/conversation_view_background.xml @@ -1,7 +1,7 @@ + android:color="?android:colorControlHighlight"> diff --git a/app/src/main/res/drawable/default_dialog_background_inset.xml b/app/src/main/res/drawable/default_dialog_background_inset.xml deleted file mode 100644 index 0ff315ebd..000000000 --- a/app/src/main/res/drawable/default_dialog_background_inset.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/destructive_dialog_text_button_background.xml b/app/src/main/res/drawable/destructive_dialog_text_button_background.xml index 1eff84a69..f3e13c800 100644 --- a/app/src/main/res/drawable/destructive_dialog_text_button_background.xml +++ b/app/src/main/res/drawable/destructive_dialog_text_button_background.xml @@ -1,11 +1,10 @@ - - - - - - - - \ No newline at end of file + + + + + + + + diff --git a/app/src/main/res/drawable/destructive_outline_button_medium_background.xml b/app/src/main/res/drawable/destructive_outline_button_medium_background.xml index 7db4da2ec..c6e01ef98 100644 --- a/app/src/main/res/drawable/destructive_outline_button_medium_background.xml +++ b/app/src/main/res/drawable/destructive_outline_button_medium_background.xml @@ -1,11 +1,13 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/drawable/filled_button_medium_background.xml b/app/src/main/res/drawable/filled_button_medium_background.xml new file mode 100644 index 000000000..10eb6de67 --- /dev/null +++ b/app/src/main/res/drawable/filled_button_medium_background.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_expand.xml b/app/src/main/res/drawable/ic_expand.xml new file mode 100644 index 000000000..3b2b816a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_message_details__refresh.xml b/app/src/main/res/drawable/ic_message_details__refresh.xml new file mode 100644 index 000000000..2aabe6fbe --- /dev/null +++ b/app/src/main/res/drawable/ic_message_details__refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_message_details__reply.xml b/app/src/main/res/drawable/ic_message_details__reply.xml new file mode 100644 index 000000000..c9e1591a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_details__reply.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_message_details__trash.xml b/app/src/main/res/drawable/ic_message_details__trash.xml new file mode 100644 index 000000000..85d421695 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_details__trash.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml new file mode 100644 index 000000000..1e72d86cb --- /dev/null +++ b/app/src/main/res/drawable/ic_next.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_pictures.xml b/app/src/main/res/drawable/ic_pictures.xml new file mode 100644 index 000000000..967d0a65b --- /dev/null +++ b/app/src/main/res/drawable/ic_pictures.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_prev.xml b/app/src/main/res/drawable/ic_prev.xml new file mode 100644 index 000000000..f72026167 --- /dev/null +++ b/app/src/main/res/drawable/ic_prev.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/mention_candidate_view_background.xml b/app/src/main/res/drawable/mention_candidate_view_background.xml index 7b179020a..4e9785a41 100644 --- a/app/src/main/res/drawable/mention_candidate_view_background.xml +++ b/app/src/main/res/drawable/mention_candidate_view_background.xml @@ -1,7 +1,7 @@ + android:color="?android:colorControlHighlight"> diff --git a/app/src/main/res/drawable/preference_bottom.xml b/app/src/main/res/drawable/preference_bottom.xml index d751c7841..b6c5f506f 100644 --- a/app/src/main/res/drawable/preference_bottom.xml +++ b/app/src/main/res/drawable/preference_bottom.xml @@ -1,5 +1,6 @@ - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/drawable/preference_middle.xml b/app/src/main/res/drawable/preference_middle.xml index 0f7229cb7..bf27aacc7 100644 --- a/app/src/main/res/drawable/preference_middle.xml +++ b/app/src/main/res/drawable/preference_middle.xml @@ -1,5 +1,6 @@ - + @@ -14,4 +15,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/drawable/preference_single.xml b/app/src/main/res/drawable/preference_single.xml index da856fcd1..7caf24a08 100644 --- a/app/src/main/res/drawable/preference_single.xml +++ b/app/src/main/res/drawable/preference_single.xml @@ -1,5 +1,6 @@ - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/drawable/preference_top.xml b/app/src/main/res/drawable/preference_top.xml index a997713e2..8f56ddc87 100644 --- a/app/src/main/res/drawable/preference_top.xml +++ b/app/src/main/res/drawable/preference_top.xml @@ -1,5 +1,6 @@ - + - \ No newline at end of file + diff --git a/app/src/main/res/drawable/profile_picture_view_large_background.xml b/app/src/main/res/drawable/profile_picture_view_large_background.xml index 90325c3fb..9b9066080 100644 --- a/app/src/main/res/drawable/profile_picture_view_large_background.xml +++ b/app/src/main/res/drawable/profile_picture_view_large_background.xml @@ -1,6 +1,9 @@ + android:shape="rectangle"> + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/prominent_filled_button_medium_background.xml b/app/src/main/res/drawable/prominent_filled_button_medium_background.xml index a06a0d11e..698a67c0a 100644 --- a/app/src/main/res/drawable/prominent_filled_button_medium_background.xml +++ b/app/src/main/res/drawable/prominent_filled_button_medium_background.xml @@ -1,11 +1,10 @@ - - - - - - - - \ No newline at end of file + + + + + + + + diff --git a/app/src/main/res/drawable/prominent_outline_button_medium_background.xml b/app/src/main/res/drawable/prominent_outline_button_medium_background.xml index ee3bec8f7..4bde2f855 100644 --- a/app/src/main/res/drawable/prominent_outline_button_medium_background.xml +++ b/app/src/main/res/drawable/prominent_outline_button_medium_background.xml @@ -1,11 +1,13 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/drawable/radial_select.xml b/app/src/main/res/drawable/radial_select.xml index a56519ed6..e09c778d0 100644 --- a/app/src/main/res/drawable/radial_select.xml +++ b/app/src/main/res/drawable/radial_select.xml @@ -1,12 +1,21 @@ - - + + + + + + + + + + + + + + + - + - - - - - \ No newline at end of file + diff --git a/app/src/main/res/drawable/setting_button_background.xml b/app/src/main/res/drawable/setting_button_background.xml index aaceb7ed5..2f177318e 100644 --- a/app/src/main/res/drawable/setting_button_background.xml +++ b/app/src/main/res/drawable/setting_button_background.xml @@ -1,7 +1,7 @@ + android:color="?android:colorControlHighlight"> diff --git a/app/src/main/res/drawable/unimportant_dialog_text_button_background.xml b/app/src/main/res/drawable/unimportant_dialog_text_button_background.xml index 1eff84a69..f3e13c800 100644 --- a/app/src/main/res/drawable/unimportant_dialog_text_button_background.xml +++ b/app/src/main/res/drawable/unimportant_dialog_text_button_background.xml @@ -1,11 +1,10 @@ - - - - - - - - \ No newline at end of file + + + + + + + + diff --git a/app/src/main/res/drawable/unimportant_outline_button_medium_background.xml b/app/src/main/res/drawable/unimportant_outline_button_medium_background.xml index 6e0de35a5..d12f7408d 100644 --- a/app/src/main/res/drawable/unimportant_outline_button_medium_background.xml +++ b/app/src/main/res/drawable/unimportant_outline_button_medium_background.xml @@ -1,11 +1,13 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/drawable/view_separator.xml b/app/src/main/res/drawable/view_separator.xml new file mode 100644 index 000000000..27dd4bc96 --- /dev/null +++ b/app/src/main/res/drawable/view_separator.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw400dp/activity_display_name.xml b/app/src/main/res/layout-sw400dp/activity_display_name.xml index 50986e7dd..4d4ff3040 100644 --- a/app/src/main/res/layout-sw400dp/activity_display_name.xml +++ b/app/src/main/res/layout-sw400dp/activity_display_name.xml @@ -33,6 +33,7 @@