mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-24 18:45:19 +00:00
Merge branch 'dev' into disappear-2
This commit is contained in:
commit
0b11e182ff
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,4 +15,4 @@ signing.properties
|
||||
ffpr
|
||||
*.sh
|
||||
pkcs11.password
|
||||
play
|
||||
app/play
|
||||
|
@ -34,6 +34,12 @@ Setting up a development environment and building from Android Studio
|
||||
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
|
||||
-----------------
|
||||
|
||||
|
311
app/build.gradle
311
app/build.gradle
@ -24,15 +24,181 @@ 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'
|
||||
|
||||
|
||||
configurations.all {
|
||||
exclude module: "commons-logging"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 354
|
||||
def canonicalVersionName = "1.17.0"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4,
|
||||
'universal' : 5]
|
||||
|
||||
android {
|
||||
compileSdkVersion androidCompileSdkVersion
|
||||
namespace 'network.loki.messenger'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'LICENSE'
|
||||
exclude 'NOTICE'
|
||||
exclude 'asm-license.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.4.7'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion androidMinimumSdkVersion
|
||||
targetSdkVersion androidTargetSdkVersion
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
project.ext.set("archivesBaseName", "session")
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
|
||||
resConfigs autoResConfig()
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
String sharedTestDir = 'src/sharedTest/java'
|
||||
test.java.srcDirs += sharedTestDir
|
||||
androidTest.java.srcDirs += sharedTestDir
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "distribution"
|
||||
productFlavors {
|
||||
play {
|
||||
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\""
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
def huaweiEnabled = project.properties['huawei'] != null
|
||||
|
||||
applicationVariants.configureEach { variant ->
|
||||
if (variant.flavorName == 'huawei') {
|
||||
variant.getPreBuildProvider().configure { task ->
|
||||
task.doFirst {
|
||||
if (!huaweiEnabled) {
|
||||
def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See BUILDING.md'
|
||||
logger.error(message)
|
||||
throw new GradleException(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.46.1")
|
||||
@ -59,11 +225,12 @@ dependencies {
|
||||
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") {
|
||||
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'
|
||||
@ -176,144 +343,6 @@ dependencies {
|
||||
implementation 'androidx.compose.material:material:1.4.3'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 354
|
||||
def canonicalVersionName = "1.17.0"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4,
|
||||
'universal' : 5]
|
||||
|
||||
android {
|
||||
compileSdkVersion androidCompileSdkVersion
|
||||
namespace 'network.loki.messenger'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'LICENSE'
|
||||
exclude 'NOTICE'
|
||||
exclude 'asm-license.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.4.7'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion androidMinimumSdkVersion
|
||||
targetSdkVersion androidTargetSdkVersion
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
project.ext.set("archivesBaseName", "session")
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
|
||||
resConfigs autoResConfig()
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
String sharedTestDir = 'src/sharedTest/java'
|
||||
test.java.srcDirs += sharedTestDir
|
||||
androidTest.java.srcDirs += sharedTestDir
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "distribution"
|
||||
productFlavors {
|
||||
play {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
website {
|
||||
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
static def getLastCommitTimestamp() {
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
return os.toString() + "000"
|
||||
|
26
app/src/huawei/AndroidManifest.xml
Normal file
26
app/src/huawei/AndroidManifest.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application tools:node="merge">
|
||||
<meta-data
|
||||
android:name="com.huawei.hms.client.appid"
|
||||
android:value="appid=107205081">
|
||||
</meta-data>
|
||||
|
||||
<meta-data
|
||||
android:name="com.huawei.hms.client.cpid"
|
||||
android:value="cpid=30061000024605000">
|
||||
</meta-data>
|
||||
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.notifications.HuaweiPushService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
96
app/src/huawei/agconnect-services.json
Normal file
96
app/src/huawei/agconnect-services.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<PushRegistry>,
|
||||
): 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) }
|
||||
}
|
||||
}
|
5
app/src/huawei/res/values/strings.xml
Normal file
5
app/src/huawei/res/values/strings.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="preferences_notifications_strategy_category_fast_mode_summary">You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers.</string>
|
||||
<string name="activity_pn_mode_fast_mode_explanation">You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers.</string>
|
||||
</resources>
|
@ -313,14 +313,6 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||
</activity>
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.notifications.PushNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
|
@ -41,6 +41,7 @@ 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;
|
||||
@ -73,10 +74,9 @@ 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;
|
||||
@ -143,8 +143,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
|
||||
@Inject LokiAPIDatabase lokiAPIDatabase;
|
||||
@Inject public Storage storage;
|
||||
@Inject Device device;
|
||||
@Inject MessageDataProvider messageDataProvider;
|
||||
@Inject TextSecurePreferences textSecurePreferences;
|
||||
@Inject PushRegistry pushRegistry;
|
||||
@Inject ConfigFactory configFactory;
|
||||
CallMessageProcessor callMessageProcessor;
|
||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||
@ -207,8 +209,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
DatabaseModule.init(this);
|
||||
MessagingModuleConfiguration.configure(this);
|
||||
super.onCreate();
|
||||
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
|
||||
messagingModuleConfiguration = new MessagingModuleConfiguration(
|
||||
this,
|
||||
storage,
|
||||
device,
|
||||
messageDataProvider,
|
||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
||||
configFactory
|
||||
@ -226,10 +230,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();
|
||||
@ -427,33 +427,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;
|
||||
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
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;
|
||||
@ -524,18 +497,14 @@ 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();
|
||||
|
@ -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<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> 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);
|
||||
}
|
||||
}
|
16
app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt
Normal file
16
app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt
Normal file
@ -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
|
||||
}
|
@ -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<RecipientDeliveryStatus> members;
|
||||
private final boolean isPushGroup;
|
||||
|
||||
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
||||
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -111,16 +111,16 @@ class SessionDialogBuilder(val context: Context) {
|
||||
text,
|
||||
contentDescription,
|
||||
R.style.Widget_Session_Button_Dialog_DestructiveText,
|
||||
listener
|
||||
)
|
||||
) { listener() }
|
||||
|
||||
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok, listener = listener)
|
||||
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button, listener = 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)
|
||||
@ -129,7 +129,7 @@ class SessionDialogBuilder(val context: Context) {
|
||||
.apply { setMargins(toPx(20, resources)) }
|
||||
setOnClickListener {
|
||||
listener.invoke()
|
||||
dismiss()
|
||||
if (dismiss) dismiss()
|
||||
}
|
||||
}.let(buttonLayout::addView)
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
public interface Unbindable {
|
||||
public void unbind();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@ 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
|
||||
@ -74,7 +73,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
additionalDisplayName = getUserDisplayName(apk)
|
||||
}
|
||||
} else if(recipient.isOpenGroupInboxRecipient) {
|
||||
val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize())
|
||||
val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
|
||||
this.publicKey = publicKey
|
||||
displayName = getUserDisplayName(publicKey)
|
||||
additionalPublicKey = null
|
||||
|
@ -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<Bitmap> bitmapReference;
|
||||
private ListenableFutureTask<Bitmap> 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<Bitmap> get() {
|
||||
Util.assertMainThread();
|
||||
|
||||
if (bitmapReference != null && bitmapReference.get() != null) {
|
||||
return new ListenableFutureTask<>(bitmapReference.get());
|
||||
} else if (task != null) {
|
||||
return task;
|
||||
} else {
|
||||
Callable<Bitmap> 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<Void, Void, Void>() {
|
||||
@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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 "";
|
||||
}
|
@ -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<SharedContact.Phone> phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size());
|
||||
List<SharedContact.Email> emails = new ArrayList<>(contact.getEmails().size());
|
||||
List<SharedContact.PostalAddress> 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<Phone> 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<Email> 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<PostalAddress> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -38,6 +38,7 @@ import androidx.activity.viewModels
|
||||
import androidx.core.text.set
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.drawToBitmap
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@ -77,7 +78,6 @@ import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||
import org.session.libsession.messaging.messages.visible.Reaction
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
@ -105,7 +105,6 @@ 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.conversation.ConversationActionBarDelegate
|
||||
import org.thoughtcrime.securesms.conversation.expiration.ExpirationSettingsActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
|
||||
@ -172,6 +171,7 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
||||
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
@ -237,11 +237,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
|
||||
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
|
||||
fromSerialized(it)
|
||||
} ?: run {
|
||||
val openGroupInboxId =
|
||||
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
|
||||
fromSerialized(GroupUtil.getEncodedOpenGroupInboxID(openGroupInboxId))
|
||||
}
|
||||
} ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, sessionId)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
@ -250,7 +246,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
} ?: finish()
|
||||
}
|
||||
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair(), contentResolver)
|
||||
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair())
|
||||
}
|
||||
private var actionMode: ActionMode? = null
|
||||
private var unreadCount = 0
|
||||
@ -309,8 +305,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
handleSwipeToReply(message)
|
||||
},
|
||||
onItemLongPress = { message, position, view ->
|
||||
if (!isMessageRequestThread() &&
|
||||
(viewModel.openGroup == null || Capability.REACTIONS.name.lowercase() in viewModel.serverCapabilities)
|
||||
if (!viewModel.isMessageRequestThread &&
|
||||
viewModel.canReactToMessages
|
||||
) {
|
||||
showEmojiPicker(message, view)
|
||||
} else {
|
||||
@ -593,26 +589,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpInputBar() {
|
||||
binding!!.inputBar.isVisible = viewModel.openGroup == null || viewModel.openGroup?.canWrite == true
|
||||
binding!!.inputBar.delegate = this
|
||||
binding!!.inputBarRecordingView.delegate = this
|
||||
val binding = binding ?: return
|
||||
binding.inputBar.isGone = viewModel.hidesInputBar()
|
||||
binding.inputBar.delegate = this
|
||||
binding.inputBarRecordingView.delegate = this
|
||||
// GIF button
|
||||
binding!!.gifButtonContainer.addView(gifButton)
|
||||
binding.gifButtonContainer.addView(gifButton)
|
||||
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
gifButton.onUp = { showGIFPicker() }
|
||||
gifButton.snIsEnabled = false
|
||||
// Document button
|
||||
binding!!.documentButtonContainer.addView(documentButton)
|
||||
binding.documentButtonContainer.addView(documentButton)
|
||||
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
documentButton.onUp = { showDocumentPicker() }
|
||||
documentButton.snIsEnabled = false
|
||||
// Library button
|
||||
binding!!.libraryButtonContainer.addView(libraryButton)
|
||||
binding.libraryButtonContainer.addView(libraryButton)
|
||||
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
libraryButton.onUp = { pickFromLibrary() }
|
||||
libraryButton.snIsEnabled = false
|
||||
// Camera button
|
||||
binding!!.cameraButtonContainer.addView(cameraButton)
|
||||
binding.cameraButtonContainer.addView(cameraButton)
|
||||
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
cameraButton.onUp = { showCamera() }
|
||||
cameraButton.snIsEnabled = false
|
||||
@ -776,7 +773,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
val recipient = viewModel.recipient ?: return false
|
||||
if (!isMessageRequestThread()) {
|
||||
if (!viewModel.isMessageRequestThread) {
|
||||
ConversationMenuHelper.onPrepareOptionsMenu(
|
||||
menu,
|
||||
menuInflater,
|
||||
@ -1080,11 +1077,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
private fun updatePlaceholder() {
|
||||
val recipient = viewModel.recipient
|
||||
?: return Log.w("Loki", "recipient was null in placeholder update")
|
||||
val blindedRecipient = viewModel.blindedRecipient
|
||||
val binding = binding ?: return
|
||||
val openGroup = viewModel.openGroup
|
||||
val (textResource, insertParam) = when {
|
||||
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
|
||||
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
|
||||
blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString()
|
||||
else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
|
||||
}
|
||||
val showPlaceholder = adapter.itemCount == 0
|
||||
|
@ -1,10 +1,8 @@
|
||||
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
|
||||
@ -22,7 +20,6 @@ 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
|
||||
@ -31,7 +28,6 @@ import java.util.UUID
|
||||
class ConversationViewModel(
|
||||
val threadId: Long,
|
||||
val edKeyPair: KeyPair?,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val repository: ConversationRepository,
|
||||
private val storage: Storage
|
||||
) : ViewModel() {
|
||||
@ -51,6 +47,15 @@ class ConversationViewModel(
|
||||
val recipient: Recipient?
|
||||
get() = _recipient.value
|
||||
|
||||
val blindedRecipient: Recipient?
|
||||
get() = _recipient.value?.let { recipient ->
|
||||
when {
|
||||
recipient.isOpenGroupOutboxRecipient -> recipient
|
||||
recipient.isOpenGroupInboxRecipient -> repository.maybeGetBlindedRecipient(recipient)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private var _openGroup: RetrieveOnce<OpenGroup> = RetrieveOnce {
|
||||
storage.getOpenGroup(threadId)
|
||||
}
|
||||
@ -66,12 +71,22 @@ class ConversationViewModel(
|
||||
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
|
||||
}
|
||||
|
||||
val isMessageRequestThread : Boolean
|
||||
get() {
|
||||
val recipient = recipient ?: return false
|
||||
return !recipient.isLocalNumber && !recipient.isGroupRecipient && !recipient.isApproved
|
||||
}
|
||||
|
||||
val canReactToMessages: Boolean
|
||||
// allow reactions if the open group is null (normal conversations) or the open group's capabilities include reactions
|
||||
get() = (openGroup == null || OpenGroupApi.Capability.REACTIONS.name.lowercase() in serverCapabilities)
|
||||
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId))
|
||||
.collect {
|
||||
val recipientExists = storage.getRecipientForThread(threadId) != null
|
||||
if (!recipientExists && _uiState.value.conversationExists) {
|
||||
repository.recipientUpdateFlow(threadId)
|
||||
.collect { recipient ->
|
||||
if (recipient == null && _uiState.value.conversationExists) {
|
||||
_uiState.update { it.copy(conversationExists = false) }
|
||||
}
|
||||
}
|
||||
@ -203,22 +218,25 @@ class ConversationViewModel(
|
||||
_recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId))
|
||||
}
|
||||
|
||||
fun hidesInputBar(): Boolean = openGroup?.canWrite != true &&
|
||||
blindedRecipient?.blocksCommunityMessageRequests == true
|
||||
|
||||
|
||||
@dagger.assisted.AssistedFactory
|
||||
interface AssistedFactory {
|
||||
fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory
|
||||
fun create(threadId: Long, edKeyPair: KeyPair?): 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: Storage
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ConversationViewModel(threadId, edKeyPair, contentResolver, repository, storage) as T
|
||||
return ConversationViewModel(threadId, edKeyPair, repository, storage) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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<VH extends RecyclerView.ViewHolder, T>
|
||||
extends CursorRecyclerViewAdapter<VH>
|
||||
{
|
||||
private static final String TAG = FastCursorRecyclerViewAdapter.class.getSimpleName();
|
||||
|
||||
private final LinkedList<T> fastRecords = new LinkedList<>();
|
||||
private final List<Long> 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<Long> releaseIdIterator = releasedRecordIds.iterator();
|
||||
|
||||
while (releaseIdIterator.hasNext()) {
|
||||
long releasedId = releaseIdIterator.next();
|
||||
Iterator<T> 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;
|
||||
}
|
||||
|
||||
}
|
@ -64,13 +64,14 @@ public class RecipientDatabase extends Database {
|
||||
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 BLOCKS_COMMUNITY_MESSAGE_REQUESTS = "blocks_community_message_requests";
|
||||
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
|
||||
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, DISAPPEARING_STATE, WRAPPER_HASH
|
||||
FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS
|
||||
};
|
||||
|
||||
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
@ -148,6 +149,11 @@ public class RecipientDatabase extends Database {
|
||||
"ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;";
|
||||
}
|
||||
|
||||
public static String getAddBlocksCommunityMessageRequests() {
|
||||
return "ALTER TABLE "+TABLE_NAME+" "+
|
||||
"ADD COLUMN "+BLOCKS_COMMUNITY_MESSAGE_REQUESTS+" INT DEFAULT 0;";
|
||||
}
|
||||
|
||||
public static final int NOTIFY_TYPE_ALL = 0;
|
||||
public static final int NOTIFY_TYPE_MENTIONS = 1;
|
||||
public static final int NOTIFY_TYPE_NONE = 2;
|
||||
@ -204,6 +210,7 @@ public class RecipientDatabase extends Database {
|
||||
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));
|
||||
boolean blocksCommunityMessageRequests = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKS_COMMUNITY_MESSAGE_REQUESTS)) == 1;
|
||||
|
||||
MaterialColor color;
|
||||
byte[] profileKey = null;
|
||||
@ -236,7 +243,7 @@ public class RecipientDatabase extends Database {
|
||||
systemPhoneLabel, systemContactUri,
|
||||
signalProfileName, signalProfileAvatar, profileSharing,
|
||||
notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
forceSmsSelection, wrapperHash));
|
||||
forceSmsSelection, wrapperHash, blocksCommunityMessageRequests));
|
||||
}
|
||||
|
||||
public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) {
|
||||
@ -393,6 +400,14 @@ public class RecipientDatabase extends Database {
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setBlocksCommunityMessageRequests(@NonNull Recipient recipient, boolean isBlocked) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(BLOCKS_COMMUNITY_MESSAGE_REQUESTS, isBlocked ? 1 : 0);
|
||||
updateOrInsert(recipient.getAddress(), contentValues);
|
||||
recipient.resolve().setBlocksCommunityMessageRequests(isBlocked);
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
private void updateOrInsert(Address address, ContentValues contentValues) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
|
@ -51,7 +51,7 @@ 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.PushNotificationAPI
|
||||
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
|
||||
@ -92,8 +92,11 @@ 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 {
|
||||
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
|
||||
@ -191,6 +194,11 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
db.setProfileKey(recipient, newProfileKey)
|
||||
}
|
||||
|
||||
override fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean) {
|
||||
val db = DatabaseComponent.get(context).recipientDatabase()
|
||||
db.setBlocksCommunityMessageRequests(recipient, blocksMessageRequests)
|
||||
}
|
||||
|
||||
override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) {
|
||||
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
|
||||
Recipient.from(context, it, false)
|
||||
@ -446,6 +454,10 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
return configFactory.canPerformChange(variant, publicKey, changeTimestampMs)
|
||||
}
|
||||
|
||||
override fun isCheckingCommunityRequests(): Boolean {
|
||||
return configFactory.user?.getCommunityMessageRequests() == true
|
||||
}
|
||||
|
||||
private fun notifyUpdates(forConfigObject: ConfigBase, messageTimestamp: Long) {
|
||||
when (forConfigObject) {
|
||||
is UserProfile -> updateUser(forConfigObject, messageTimestamp)
|
||||
@ -618,7 +630,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
val expireTimer = group.disappearingTimer
|
||||
setExpirationTimer(groupId, expireTimer.toInt())
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, group.sessionId, localUserPublicKey)
|
||||
PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey)
|
||||
// Notify the user
|
||||
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||
threadDb.setDate(threadID, formationTimestamp)
|
||||
@ -1486,7 +1498,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
val blindedId = when {
|
||||
recipient.isGroupRecipient -> null
|
||||
recipient.isOpenGroupInboxRecipient -> {
|
||||
GroupUtil.getDecodedOpenGroupInbox(address)
|
||||
GroupUtil.getDecodedOpenGroupInboxSessionId(address)
|
||||
}
|
||||
else -> {
|
||||
if (SessionId(address).prefix == IdPrefix.BLINDED) {
|
||||
@ -1614,18 +1626,14 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
if (mapping.sessionId != null) {
|
||||
return mapping
|
||||
}
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDb.readerFor(threadDb.conversationList).use { reader ->
|
||||
while (reader.next != null) {
|
||||
val recipient = reader.current.recipient
|
||||
val sessionId = recipient.address.serialize()
|
||||
if (!recipient.isGroupRecipient && SodiumUtilities.sessionId(sessionId, blindedId, serverPublicKey)) {
|
||||
val contactMapping = mapping.copy(sessionId = sessionId)
|
||||
getAllContacts().forEach { contact ->
|
||||
val sessionId = SessionId(contact.sessionID)
|
||||
if (sessionId.prefix == IdPrefix.STANDARD && SodiumUtilities.sessionId(sessionId.hexString, blindedId, serverPublicKey)) {
|
||||
val contactMapping = mapping.copy(sessionId = sessionId.hexString)
|
||||
db.addBlindedIdMapping(contactMapping)
|
||||
return contactMapping
|
||||
}
|
||||
}
|
||||
}
|
||||
db.getBlindedIdMappingsExceptFor(server).forEach {
|
||||
if (SodiumUtilities.sessionId(it.sessionId!!, blindedId, serverPublicKey)) {
|
||||
val otherMapping = mapping.copy(sessionId = it.sessionId)
|
||||
|
@ -50,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;
|
||||
|
@ -90,9 +90,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV41 = 62;
|
||||
private static final int lokiV42 = 63;
|
||||
private static final int lokiV43 = 64;
|
||||
private static final int lokiV44 = 65;
|
||||
|
||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV43;
|
||||
private static final int DATABASE_VERSION = lokiV44;
|
||||
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";
|
||||
@ -360,6 +361,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
|
||||
db.execSQL(RecipientDatabase.getAddWrapperHash());
|
||||
db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -603,6 +605,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV43) {
|
||||
db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV44) {
|
||||
db.execSQL(RecipientDatabase.getCreateDisappearingStateCommand());
|
||||
db.execSQL(ExpirationConfigurationDatabase.CREATE_EXPIRATION_CONFIGURATION_TABLE_COMMAND);
|
||||
db.execSQL(ExpirationConfigurationDatabase.MIGRATE_GROUP_CONVERSATION_EXPIRY_TYPE_COMMAND);
|
||||
|
@ -0,0 +1,17 @@
|
||||
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
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ContentModule {
|
||||
|
||||
@Provides
|
||||
fun providesContentResolver(@ApplicationContext context: Context) =context.contentResolver
|
||||
|
||||
}
|
@ -3,7 +3,7 @@ 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.PushNotificationAPI
|
||||
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
|
||||
@ -24,7 +24,7 @@ object ClosedGroupManager {
|
||||
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
PushRegistryV1.unsubscribeGroup(closedGroupPublicKey = groupPublicKey, publicKey = userPublicKey)
|
||||
// Stop polling
|
||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||
storage.cancelPendingMessageSendJobs(threadId)
|
||||
|
@ -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))
|
||||
|
@ -67,6 +67,7 @@ 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
|
||||
@ -106,6 +107,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
@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<GlobalSearchViewModel>()
|
||||
private val homeViewModel by viewModels<HomeViewModel>()
|
||||
@ -230,8 +232,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
(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()
|
||||
@ -298,13 +299,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
EventBus.getDefault().register(this@HomeActivity)
|
||||
if (intent.hasExtra(FROM_ONBOARDING)
|
||||
&& intent.getBooleanExtra(FROM_ONBOARDING, false)
|
||||
&& !(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()
|
||||
) {
|
||||
&& intent.getBooleanExtra(FROM_ONBOARDING, false)) {
|
||||
if ((getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
||||
.execute()
|
||||
}
|
||||
configFactory.user?.let { user ->
|
||||
if (!user.isBlockCommunityMessageRequestsSet()) {
|
||||
user.setCommunityMessageRequests(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInputFocusChanged(hasFocus: Boolean) {
|
||||
|
@ -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<Attachment> thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaTypes.IMAGE_JPEG);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -54,7 +54,7 @@ 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;
|
||||
|
@ -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<InstanceIdResult>)->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)
|
||||
}
|
||||
}
|
@ -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<Map<*, *>, Exception> {
|
||||
return OnionRequestAPI.sendOnionRequest(request, server, pnServerPublicKey, Version.V2).map { response ->
|
||||
JsonUtil.fromJson(response.body, Map::class.java)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
interface PushManager {
|
||||
fun refresh(force: Boolean)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<String, String>?) {
|
||||
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<String, String>.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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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<Int> = 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()
|
||||
}
|
||||
}
|
@ -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<Int>
|
||||
): Promise<SubscriptionResponse, Exception> {
|
||||
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<SubscriptionResponse>("subscribe", requestParameters) success {
|
||||
Log.d(TAG, "registerV2 success")
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(
|
||||
device: Device,
|
||||
token: String,
|
||||
userPublicKey: String,
|
||||
userEdKey: KeyPair
|
||||
): Promise<UnsubscribeResponse, Exception> {
|
||||
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<UnsubscribeResponse>("unsubscribe", requestParameters) success {
|
||||
Log.d(TAG, "unregisterV2 success")
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T: Response> retryResponseBody(path: String, requestParameters: String): Promise<T, Exception> =
|
||||
retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) }
|
||||
|
||||
private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
|
||||
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<T>(it) }
|
||||
.also { if (it.isFailure()) throw Exception("error: ${it.message}.") }
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
interface TokenFetcher {
|
||||
suspend fun fetch(): String?
|
||||
}
|
@ -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()
|
||||
}
|
@ -12,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
|
||||
@ -19,6 +20,8 @@ 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
|
||||
@ -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
|
||||
|
||||
@ -158,10 +166,10 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
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)
|
||||
|
@ -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) {
|
||||
|
@ -1,182 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
import static org.thoughtcrime.securesms.preferences.ListPreferenceDialogKt.listPreferenceDialog;
|
||||
|
||||
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);
|
||||
listPreferenceDialog(getContext(), listPreference, () -> {
|
||||
initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF));
|
||||
return null;
|
||||
});
|
||||
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<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
ApplicationContext.getInstance(getActivity()).messageNotifier.updateNotification(getActivity());
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
return super.onPreferenceChange(preference, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<Preference>(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceChangeListener = RingtoneSummaryListener()
|
||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceChangeListener = NotificationPrivacyListener()
|
||||
findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF)!!.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
||||
NotificationChannels.updateMessageVibrate(requireContext(), newValue as Boolean)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(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<Preference>(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<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) as ListPreference?)
|
||||
if (NotificationChannels.supported()) {
|
||||
findPreference<Preference>(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<Preference>(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<Uri>(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<Void?, Void?, Void?>() {
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.os.Bundle
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PrivacySettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
|
@ -8,6 +8,9 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
@ -15,13 +18,19 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.isPasswo
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setScreenLockEnabled
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
|
||||
|
||||
@Inject lateinit var configFactory: ConfigFactory
|
||||
|
||||
override fun onCreate(paramBundle: Bundle?) {
|
||||
super.onCreate(paramBundle)
|
||||
findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK)!!
|
||||
@ -30,6 +39,33 @@ class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
|
||||
.onPreferenceChangeListener = TypingIndicatorsToggleListener()
|
||||
findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!!
|
||||
.onPreferenceChangeListener = CallToggleListener(this) { setCall(it) }
|
||||
findPreference<PreferenceCategory>(getString(R.string.preferences__message_requests_category))?.let { category ->
|
||||
when (val user = configFactory.user) {
|
||||
null -> category.isVisible = false
|
||||
else -> SwitchPreferenceCompat(requireContext()).apply {
|
||||
key = TextSecurePreferences.ALLOW_MESSAGE_REQUESTS
|
||||
preferenceDataStore = object : PreferenceDataStore() {
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||
if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) {
|
||||
return user.getCommunityMessageRequests()
|
||||
}
|
||||
return super.getBoolean(key, defValue)
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) {
|
||||
user.setCommunityMessageRequests(value)
|
||||
return
|
||||
}
|
||||
super.putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
title = getString(R.string.preferences__message_requests_title)
|
||||
summary = getString(R.string.preferences__message_requests_summary)
|
||||
}.let(category::addPreference)
|
||||
}
|
||||
}
|
||||
initializeVisibility()
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ class ShareLogsDialog : DialogFragment() {
|
||||
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) { shareLogs() }
|
||||
button(R.string.share, dismiss = false) { shareLogs() }
|
||||
cancelButton { dismiss() }
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.Emoji;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Contains the Emojis that have been used in reactions for a given message.
|
||||
*/
|
||||
class ThisMessageEmojiPageModel implements EmojiPageModel {
|
||||
|
||||
private final List<String> emoji;
|
||||
|
||||
ThisMessageEmojiPageModel(@NonNull List<String> 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<String> getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Emoji> 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;
|
||||
}
|
||||
}
|
@ -1,6 +1,12 @@
|
||||
package org.thoughtcrime.securesms.repository
|
||||
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import app.cash.copper.flow.observeQuery
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
@ -16,6 +22,7 @@ import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
@ -37,6 +44,8 @@ import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface ConversationRepository {
|
||||
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||
fun maybeGetBlindedRecipient(recipient: Recipient): Recipient?
|
||||
fun recipientUpdateFlow(threadId: Long): Flow<Recipient?>
|
||||
fun saveDraft(threadId: Long, text: String)
|
||||
fun getDraft(threadId: Long): String?
|
||||
fun clearDrafts(threadId: Long)
|
||||
@ -77,6 +86,7 @@ interface ConversationRepository {
|
||||
}
|
||||
|
||||
class DefaultConversationRepository @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val textSecurePreferences: TextSecurePreferences,
|
||||
private val messageDataProvider: MessageDataProvider,
|
||||
private val threadDb: ThreadDatabase,
|
||||
@ -90,13 +100,29 @@ class DefaultConversationRepository @Inject constructor(
|
||||
private val lokiMessageDb: LokiMessageDatabase,
|
||||
private val sessionJobDb: SessionJobDatabase,
|
||||
private val configDb: ExpirationConfigurationDatabase,
|
||||
private val configFactory: ConfigFactory
|
||||
private val configFactory: ConfigFactory,
|
||||
private val contentResolver: ContentResolver,
|
||||
) : ConversationRepository {
|
||||
|
||||
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
|
||||
return threadDb.getRecipientForThreadId(threadId)
|
||||
}
|
||||
|
||||
override fun maybeGetBlindedRecipient(recipient: Recipient): Recipient? {
|
||||
if (!recipient.isOpenGroupInboxRecipient) return null
|
||||
return Recipient.from(
|
||||
context,
|
||||
Address.fromSerialized(GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
override fun recipientUpdateFlow(threadId: Long): Flow<Recipient?> {
|
||||
return contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)).map {
|
||||
maybeGetRecipientForThreadId(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveDraft(threadId: Long, text: String) {
|
||||
if (text.isEmpty()) return
|
||||
val drafts = DraftDatabase.Drafts()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package org.thoughtcrime.securesms.webrtc
|
||||
|
||||
enum class AudioEvent {
|
||||
|
||||
}
|
@ -644,6 +644,9 @@
|
||||
<string name="preferences_notifications__priority">Priority</string>
|
||||
<string name="preferences_app_protection__screenshot_notifications">Screenshot Notifications</string>
|
||||
<string name="preferences_app_protected__screenshot_notifications_summary">Receive a notification when a contact takes a screenshot of a one-to-one chat.</string>
|
||||
<string name="preferences__message_requests_category">Message Requests</string>
|
||||
<string name="preferences__message_requests_title">Community Message Requests</string>
|
||||
<string name="preferences__message_requests_summary">Allow message requests from Community conversations</string>
|
||||
<!-- **************************************** -->
|
||||
<!-- menus -->
|
||||
<!-- **************************************** -->
|
||||
@ -1066,6 +1069,7 @@
|
||||
<string name="activity_home_outdated_client_config">Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.</string>
|
||||
|
||||
<string name="activity_conversation_empty_state_read_only">There are no messages in <b>%s</b>.</string>
|
||||
<string name="activity_conversation_empty_state_blocks_community_requests"><b>%s</b> has message requests from Community conversations turned off, so you cannot send them a message.</string>
|
||||
<string name="activity_conversation_empty_state_note_to_self">You have no messages in Note to Self.</string>
|
||||
<string name="activity_conversation_empty_state_default">You have no messages from <b>%s</b>.\nSend a message to start the conversation!</string>
|
||||
|
||||
|
@ -20,6 +20,12 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/preferences__message_requests_category"
|
||||
android:key="@string/preferences__message_requests_category"
|
||||
android:persistent="false">
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/preferences__read_receipts">
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
|
16
app/src/play/AndroidManifest.xml
Normal file
16
app/src/play/AndroidManifest.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application tools:node="merge">
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.notifications.FirebasePushService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -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 FirebaseBindingModule {
|
||||
@Binds
|
||||
abstract fun bindTokenFetcher(tokenFetcher: FirebaseTokenFetcher): TokenFetcher
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Log
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "FirebasePushNotificationService"
|
||||
@AndroidEntryPoint
|
||||
class FirebasePushService : FirebaseMessagingService() {
|
||||
|
||||
@Inject lateinit var prefs: TextSecurePreferences
|
||||
@Inject lateinit var pushReceiver: PushReceiver
|
||||
@Inject lateinit var pushRegistry: PushRegistry
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
if (token == prefs.getPushToken()) return
|
||||
|
||||
pushRegistry.register(token)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
Log.d(TAG, "Received a push notification.")
|
||||
pushReceiver.onPush(message.data)
|
||||
}
|
||||
|
||||
override fun onDeletedMessages() {
|
||||
Log.d(TAG, "Called onDeletedMessages.")
|
||||
pushRegistry.refresh(true)
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FirebaseTokenFetcher @Inject constructor(): TokenFetcher {
|
||||
override suspend fun fetch() = withContext(Dispatchers.IO) {
|
||||
FirebaseInstanceId.getInstance().instanceId
|
||||
.also(Tasks::await)
|
||||
.takeIf { isActive } // don't 'complete' task if we were canceled
|
||||
?.run { result?.token ?: throw exception!! }
|
||||
}
|
||||
}
|
@ -1,42 +1,46 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.hamcrest.CoreMatchers.endsWith
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.CoreMatchers.notNullValue
|
||||
import org.hamcrest.CoreMatchers.nullValue
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.anyLong
|
||||
import org.mockito.Mockito.anySet
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.BaseViewModelTest
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import org.thoughtcrime.securesms.repository.ResultOf
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
class ConversationViewModelTest: BaseViewModelTest() {
|
||||
|
||||
private val repository = mock(ConversationRepository::class.java)
|
||||
private val storage = mock(Storage::class.java)
|
||||
private val repository = mock<ConversationRepository>()
|
||||
private val storage = mock<Storage>()
|
||||
|
||||
private val threadId = 123L
|
||||
private val edKeyPair = mock(KeyPair::class.java)
|
||||
private val edKeyPair = mock<KeyPair>()
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
private val viewModel: ConversationViewModel by lazy {
|
||||
ConversationViewModel(threadId, edKeyPair, mock(), repository, storage)
|
||||
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = mock(Recipient::class.java)
|
||||
recipient = mock()
|
||||
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
|
||||
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -79,7 +83,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `should delete locally`() {
|
||||
val message = mock(MessageRecord::class.java)
|
||||
val message = mock<MessageRecord>()
|
||||
|
||||
viewModel.deleteLocally(message)
|
||||
|
||||
@ -88,7 +92,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest {
|
||||
val message = mock(MessageRecord::class.java)
|
||||
val message = mock<MessageRecord>()
|
||||
val error = Throwable()
|
||||
whenever(repository.deleteForEveryone(anyLong(), any(), any()))
|
||||
.thenReturn(ResultOf.Failure(error))
|
||||
@ -101,7 +105,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
@Test
|
||||
fun `should emit error message on failure to delete messages without unsend request`() =
|
||||
runBlockingTest {
|
||||
val message = mock(MessageRecord::class.java)
|
||||
val message = mock<MessageRecord>()
|
||||
val error = Throwable()
|
||||
whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet()))
|
||||
.thenReturn(ResultOf.Failure(error))
|
||||
@ -181,4 +185,30 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open group recipient should have no blinded recipient`() {
|
||||
whenever(recipient.isOpenGroupRecipient).thenReturn(true)
|
||||
whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false)
|
||||
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
|
||||
assertThat(viewModel.blindedRecipient, nullValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `local recipient should have input and no blinded recipient`() {
|
||||
whenever(recipient.isLocalNumber).thenReturn(true)
|
||||
assertThat(viewModel.hidesInputBar(), equalTo(false))
|
||||
assertThat(viewModel.blindedRecipient, nullValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `contact recipient should hide input bar if not accepting requests`() {
|
||||
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(true)
|
||||
val blinded = mock<Recipient> {
|
||||
whenever(it.blocksCommunityMessageRequests).thenReturn(true)
|
||||
}
|
||||
whenever(repository.maybeGetBlindedRecipient(recipient)).thenReturn(blinded)
|
||||
assertThat(viewModel.blindedRecipient, notNullValue())
|
||||
assertThat(viewModel.hidesInputBar(), equalTo(true))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class NoOpPushModule {
|
||||
@Binds
|
||||
abstract fun bindTokenFetcher(tokenFetcher: NoOpTokenFetcher): TokenFetcher
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NoOpTokenFetcher @Inject constructor() : TokenFetcher {
|
||||
override suspend fun fetch(): String? = null
|
||||
}
|
20
build.gradle
20
build.gradle
@ -2,13 +2,24 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
if (project.hasProperty('huawei')) maven {
|
||||
url 'https://developer.huawei.com/repo/'
|
||||
content {
|
||||
includeGroup 'com.huawei.agconnect'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||
classpath "com.google.gms:google-services:$googleServicesVersion"
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
classpath "com.squareup:javapoet:1.13.0"
|
||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
||||
if (project.hasProperty('huawei')) classpath 'com.huawei.agconnect:agcp:1.9.1.300'
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +63,15 @@ allprojects {
|
||||
}
|
||||
jcenter()
|
||||
maven { url "https://jitpack.io" }
|
||||
if (project.hasProperty('huawei')) maven {
|
||||
url 'https://developer.huawei.com/repo/'
|
||||
content {
|
||||
includeGroup 'com.huawei.android.hms'
|
||||
includeGroup 'com.huawei.agconnect'
|
||||
includeGroup 'com.huawei.hmf'
|
||||
includeGroup 'com.huawei.hms'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.ext {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 7eb87028355bfc89950102c52d5b2927a25b2e22
|
||||
Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d
|
@ -120,3 +120,33 @@ Java_network_loki_messenger_libsession_1util_UserProfile_getNtsExpiry(JNIEnv *en
|
||||
auto expiry = util::serialize_expiry(env, session::config::expiration_mode::after_send, std::chrono::seconds(*nts_expiry));
|
||||
return expiry;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_UserProfile_getCommunityMessageRequests(
|
||||
JNIEnv *env, jobject thiz) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto profile = ptrToProfile(env, thiz);
|
||||
auto blinded_msg_requests = profile->get_blinded_msgreqs();
|
||||
if (blinded_msg_requests.has_value()) {
|
||||
return *blinded_msg_requests;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_UserProfile_setCommunityMessageRequests(
|
||||
JNIEnv *env, jobject thiz, jboolean blocks) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto profile = ptrToProfile(env, thiz);
|
||||
profile->set_blinded_msgreqs(std::optional{(bool)blocks});
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_UserProfile_isBlockCommunityMessageRequestsSet(
|
||||
JNIEnv *env, jobject thiz) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto profile = ptrToProfile(env, thiz);
|
||||
return profile->get_blinded_msgreqs().has_value();
|
||||
}
|
@ -129,6 +129,9 @@ class UserProfile(pointer: Long) : ConfigBase(pointer) {
|
||||
external fun getNtsPriority(): Int
|
||||
external fun setNtsExpiry(expiryMode: ExpiryMode)
|
||||
external fun getNtsExpiry(): ExpiryMode
|
||||
external fun getCommunityMessageRequests(): Boolean
|
||||
external fun setCommunityMessageRequests(blocks: Boolean)
|
||||
external fun isBlockCommunityMessageRequestsSet(): Boolean
|
||||
}
|
||||
|
||||
class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
id 'kotlinx-serialization'
|
||||
}
|
||||
|
||||
android {
|
||||
@ -41,6 +42,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
|
@ -43,6 +43,7 @@ interface StorageProtocol {
|
||||
fun getUserProfile(): Profile
|
||||
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
|
||||
fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
|
||||
fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean)
|
||||
fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
|
||||
fun clearUserPic()
|
||||
// Signal
|
||||
@ -232,4 +233,5 @@ interface StorageProtocol {
|
||||
fun notifyConfigUpdates(forConfigObject: ConfigBase, messageTimestamp: Long)
|
||||
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
|
||||
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
|
||||
fun isCheckingCommunityRequests(): Boolean
|
||||
}
|
||||
|
@ -5,10 +5,12 @@ import com.goterl.lazysodium.utils.KeyPair
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsession.utilities.Device
|
||||
|
||||
class MessagingModuleConfiguration(
|
||||
val context: Context,
|
||||
val storage: StorageProtocol,
|
||||
val device: Device,
|
||||
val messageDataProvider: MessageDataProvider,
|
||||
val getUserED25519KeyPair: () -> KeyPair?,
|
||||
val configFactory: ConfigFactoryProtocol
|
||||
|
@ -3,12 +3,11 @@ package org.session.libsession.messaging.jobs
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.Server
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
@ -31,23 +30,27 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
||||
}
|
||||
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val server = PushNotificationAPI.server
|
||||
val server = Server.LEGACY
|
||||
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
|
||||
val url = "${server}/notify"
|
||||
val url = "${server.url}/notify"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
retryIfNeeded(4) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V2
|
||||
) success { response ->
|
||||
when (response.code) {
|
||||
null, 0 -> Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: ${response.message}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
||||
} fail { exception ->
|
||||
Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: $exception.")
|
||||
}
|
||||
}.success {
|
||||
} success {
|
||||
handleSuccess(dispatcherName)
|
||||
}. fail {
|
||||
} fail {
|
||||
handleFailure(dispatcherName, it)
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ class VisibleMessage(
|
||||
var profile: Profile? = null,
|
||||
var openGroupInvitation: OpenGroupInvitation? = null,
|
||||
var reaction: Reaction? = null,
|
||||
var hasMention: Boolean = false
|
||||
var hasMention: Boolean = false,
|
||||
var blocksMessageRequests: Boolean = false
|
||||
) : Message() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
@ -71,6 +72,9 @@ class VisibleMessage(
|
||||
val reaction = Reaction.fromProto(reactionProto)
|
||||
result.reaction = reaction
|
||||
}
|
||||
|
||||
result.blocksMessageRequests = with (dataMessage) { hasBlocksCommunityMessageRequests() && blocksCommunityMessageRequests }
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -130,6 +134,8 @@ class VisibleMessage(
|
||||
return null
|
||||
}
|
||||
}
|
||||
// Community blocked message requests flag
|
||||
dataMessage.blocksCommunityMessageRequests = blocksMessageRequests
|
||||
// Sync target
|
||||
if (syncTarget != null) {
|
||||
dataMessage.syncTarget = syncTarget
|
||||
|
@ -753,7 +753,8 @@ object OpenGroupApi {
|
||||
)
|
||||
}
|
||||
val serverCapabilities = storage.getServerCapabilities(server)
|
||||
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
|
||||
val isAcceptingCommunityRequests = storage.isCheckingCommunityRequests()
|
||||
if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && isAcceptingCommunityRequests) {
|
||||
requests.add(
|
||||
if (lastInboxMessageId == null) {
|
||||
BatchRequestInfo(
|
||||
|
@ -30,6 +30,7 @@ import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.crypto.PushTransportDetails
|
||||
@ -266,9 +267,16 @@ object MessageSender {
|
||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = SnodeAPI.nowWithOffset
|
||||
}
|
||||
// Attach the blocks message requests info
|
||||
configFactory.user?.let { user ->
|
||||
if (message is VisibleMessage) {
|
||||
message.blocksMessageRequests = !user.getCommunityMessageRequests()
|
||||
}
|
||||
}
|
||||
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
||||
var serverCapabilities = listOf<String>()
|
||||
var blindedPublicKey: ByteArray? = null
|
||||
@ -479,8 +487,8 @@ object MessageSender {
|
||||
}
|
||||
|
||||
// Closed groups
|
||||
fun createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
return create(name, members)
|
||||
fun createClosedGroup(device: Device, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
return create(device, name, members)
|
||||
}
|
||||
|
||||
fun explicitNameChange(groupPublicKey: String, newName: String) {
|
||||
|
@ -8,11 +8,12 @@ import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
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.Device
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.ecc.Curve
|
||||
@ -32,7 +33,11 @@ const val groupSizeLimit = 100
|
||||
|
||||
val pendingKeyPairs = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
fun MessageSender.create(
|
||||
device: Device,
|
||||
name: String,
|
||||
members: Collection<String>
|
||||
): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
@ -88,7 +93,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
// Add the group to the config now that it was successfully created
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
PushRegistryV1.register(device = device, publicKey = userPublicKey)
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||
// Fulfill the promise
|
||||
|
@ -25,7 +25,7 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||
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.PushNotificationAPI
|
||||
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
|
||||
@ -397,6 +397,10 @@ fun MessageReceiver.handleVisibleMessage(
|
||||
profileManager.setProfilePicture(context, recipient, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
if (userPublicKey != messageSender && !isUserBlindedSender) {
|
||||
storage.setBlocksCommunityMessageRequests(recipient, message.blocksMessageRequests)
|
||||
}
|
||||
}
|
||||
// Parse quote if needed
|
||||
var quoteModel: QuoteModel? = null
|
||||
@ -649,10 +653,14 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
||||
// Set expiration timer
|
||||
storage.setExpirationTimer(groupID, expireTimer)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Create thread
|
||||
val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.setThreadDate(threadId, formationTimestamp)
|
||||
PushRegistryV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey)
|
||||
// Notify the user
|
||||
if (userPublicKey == sender && !groupExists) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
||||
} else if (userPublicKey != sender) {
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
|
||||
}
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||
}
|
||||
@ -958,7 +966,7 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou
|
||||
storage.setActive(groupID, false)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
PushRegistryV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey)
|
||||
// Stop polling
|
||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||
|
||||
|
@ -0,0 +1,126 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import com.goterl.lazysodium.utils.Key
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server.
|
||||
* Changing the variable names will break how data is serialized/deserialized.
|
||||
* If it's less than ideally named we can use [SerialName], such as for the push metadata which uses
|
||||
* single-letter keys to be as compact as possible.
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionRequest(
|
||||
/** the 33-byte account being subscribed to; typically a session ID */
|
||||
val pubkey: String,
|
||||
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
|
||||
val session_ed25519: String?,
|
||||
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
|
||||
val subkey_tag: String? = null,
|
||||
/** array of integer namespaces to subscribe to, **must be sorted in ascending order** */
|
||||
val namespaces: List<Int>,
|
||||
/** if provided and true then notifications will include the body of the message (as long as it isn't too large) */
|
||||
val data: Boolean,
|
||||
/** the signature unix timestamp in seconds, not ms */
|
||||
val sig_ts: Long,
|
||||
/** the 64-byte ed25519 signature */
|
||||
val signature: String,
|
||||
/** the string identifying the notification service, "firebase" for android (currently) */
|
||||
val service: String,
|
||||
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
|
||||
val service_info: Map<String, String>,
|
||||
/** 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 via libsodium using this key.
|
||||
* persist it on device */
|
||||
val enc_key: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UnsubscriptionRequest(
|
||||
/** the 33-byte account being subscribed to; typically a session ID */
|
||||
val pubkey: String,
|
||||
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
|
||||
val session_ed25519: String?,
|
||||
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
|
||||
val subkey_tag: String? = null,
|
||||
/** the signature unix timestamp in seconds, not ms */
|
||||
val sig_ts: Long,
|
||||
/** the 64-byte ed25519 signature */
|
||||
val signature: String,
|
||||
/** the string identifying the notification service, "firebase" for android (currently) */
|
||||
val service: String,
|
||||
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
|
||||
val service_info: Map<String, String>,
|
||||
)
|
||||
|
||||
/** invalid values, missing reuqired arguments etc, details in message */
|
||||
private const val UNPARSEABLE_ERROR = 1
|
||||
/** the "service" value is not active / valid */
|
||||
private const val SERVICE_NOT_AVAILABLE = 2
|
||||
/** something getting wrong internally talking to the backend */
|
||||
private const val SERVICE_TIMEOUT = 3
|
||||
/** other error processing the subscription (details in the message) */
|
||||
private const val GENERIC_ERROR = 4
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionResponse(
|
||||
override val error: Int? = null,
|
||||
override val message: String? = null,
|
||||
override val success: Boolean? = null,
|
||||
val added: Boolean? = null,
|
||||
val updated: Boolean? = null,
|
||||
): Response
|
||||
|
||||
@Serializable
|
||||
data class UnsubscribeResponse(
|
||||
override val error: Int? = null,
|
||||
override val message: String? = null,
|
||||
override val success: Boolean? = null,
|
||||
val removed: Boolean? = null,
|
||||
): Response
|
||||
|
||||
interface Response {
|
||||
val error: Int?
|
||||
val message: String?
|
||||
val success: Boolean?
|
||||
fun isSuccess() = success == true && error == null
|
||||
fun isFailure() = !isSuccess()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationMetadata(
|
||||
/** Account ID (such as Session ID or closed group ID) where the message arrived **/
|
||||
@SerialName("@")
|
||||
val account: String,
|
||||
|
||||
/** The hash of the message in the swarm. */
|
||||
@SerialName("#")
|
||||
val msg_hash: String,
|
||||
|
||||
/** The swarm namespace in which this message arrived. */
|
||||
@SerialName("n")
|
||||
val namespace: Int,
|
||||
|
||||
/** The length of the message data. This is always included, even if the message content
|
||||
* itself was too large to fit into the push notification. */
|
||||
@SerialName("l")
|
||||
val data_len: Int,
|
||||
|
||||
/** This will be true if the data was omitted because it was too long to fit in a push
|
||||
* notification (around 2.5kB of raw data), in which case the push notification includes
|
||||
* only this metadata but not the message content itself. */
|
||||
@SerialName("B")
|
||||
val data_too_long : Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationServerObject(
|
||||
val enc_payload: String,
|
||||
val spns: Int,
|
||||
) {
|
||||
fun decryptPayload(key: Key): Any {
|
||||
|
||||
TODO()
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushNotificationAPI {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val server = "https://live.apns.getsession.org"
|
||||
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
private val maxRetryCount = 4
|
||||
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
||||
|
||||
enum class ClosedGroupOperation {
|
||||
Subscribe, Unsubscribe;
|
||||
|
||||
val rawValue: String
|
||||
get() {
|
||||
return when (this) {
|
||||
Subscribe -> "subscribe_closed_group"
|
||||
Unsubscribe -> "unsubscribe_closed_group"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(token: String) {
|
||||
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) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, false)
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun register(token: String, publicKey: String, 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) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["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: ${response.info["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 = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun performOperation(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) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.Promise
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.OnionResponse
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.emptyPromise
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.sideEffect
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushRegistryV1 {
|
||||
private val TAG = PushRegistryV1::class.java.name
|
||||
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
private const val maxRetryCount = 4
|
||||
|
||||
private val server = Server.LEGACY
|
||||
|
||||
fun register(
|
||||
device: Device,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
token: String? = TextSecurePreferences.getPushToken(context),
|
||||
publicKey: String? = TextSecurePreferences.getLocalNumber(context),
|
||||
legacyGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
): Promise<*, Exception> = when {
|
||||
isPushEnabled -> retryIfNeeded(maxRetryCount) {
|
||||
Log.d(TAG, "register() called")
|
||||
doRegister(token, publicKey, device, legacyGroupPublicKeys)
|
||||
} fail { exception ->
|
||||
Log.d(TAG, "Couldn't register for FCM due to error", exception)
|
||||
}
|
||||
|
||||
else -> emptyPromise()
|
||||
}
|
||||
|
||||
private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection<String>): Promise<*, Exception> {
|
||||
Log.d(TAG, "doRegister() called")
|
||||
|
||||
token ?: return emptyPromise()
|
||||
publicKey ?: return emptyPromise()
|
||||
|
||||
val parameters = mapOf(
|
||||
"token" to token,
|
||||
"pubKey" to publicKey,
|
||||
"device" to device.value,
|
||||
"legacyGroupPublicKeys" to legacyGroupPublicKeys
|
||||
)
|
||||
|
||||
val url = "${server.url}/register_legacy_groups_only"
|
||||
val body = RequestBody.create(
|
||||
MediaType.get("application/json"),
|
||||
JsonUtil.toJson(parameters)
|
||||
)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return sendOnionRequest(request) sideEffect { response ->
|
||||
when (response.code) {
|
||||
null, 0 -> throw Exception("error: ${response.message}.")
|
||||
}
|
||||
} success {
|
||||
Log.d(TAG, "registerV1 success")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager.
|
||||
*/
|
||||
fun unregister(): Promise<*, Exception> {
|
||||
Log.d(TAG, "unregisterV1 requested")
|
||||
|
||||
val token = TextSecurePreferences.getPushToken(context) ?: emptyPromise()
|
||||
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val parameters = mapOf("token" to token)
|
||||
val url = "${server.url}/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
sendOnionRequest(request) success {
|
||||
when (it.code) {
|
||||
null, 0 -> Log.d(TAG, "error: ${it.message}.")
|
||||
else -> Log.d(TAG, "unregisterV1 success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy Closed Groups
|
||||
|
||||
fun subscribeGroup(
|
||||
closedGroupPublicKey: String,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
) = if (isPushEnabled) {
|
||||
performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey)
|
||||
} else emptyPromise()
|
||||
|
||||
fun unsubscribeGroup(
|
||||
closedGroupPublicKey: String,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
) = if (isPushEnabled) {
|
||||
performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey)
|
||||
} else emptyPromise()
|
||||
|
||||
private fun performGroupOperation(
|
||||
operation: String,
|
||||
closedGroupPublicKey: String,
|
||||
publicKey: String
|
||||
): Promise<*, Exception> {
|
||||
val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey)
|
||||
val url = "${server.url}/$operation"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
sendOnionRequest(request) sideEffect {
|
||||
when (it.code) {
|
||||
0, null -> throw Exception(it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOnionRequest(request: Request): Promise<OnionResponse, Exception> = OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V2
|
||||
)
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
enum class Server(val url: String, val publicKey: String) {
|
||||
LATEST("https://push.getsession.org", "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"),
|
||||
LEGACY("https://live.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049")
|
||||
}
|
@ -205,7 +205,7 @@ object SodiumUtilities {
|
||||
}
|
||||
|
||||
fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? {
|
||||
val plaintextSize = ciphertext.size - AEAD.CHACHA20POLY1305_ABYTES
|
||||
val plaintextSize = ciphertext.size - AEAD.XCHACHA20POLY1305_IETF_ABYTES
|
||||
val plaintext = ByteArray(plaintextSize)
|
||||
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||
plaintext,
|
||||
|
@ -686,4 +686,7 @@ enum class Version(val value: String) {
|
||||
data class OnionResponse(
|
||||
val info: Map<*, *>,
|
||||
val body: ByteArray? = null
|
||||
)
|
||||
) {
|
||||
val code: Int? get() = info["code"] as? Int
|
||||
val message: String? get() = info["message"] as? String
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
enum class Device(val value: String, val service: String = value) {
|
||||
ANDROID("android", "firebase"),
|
||||
HUAWEI("huawei");
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import java.io.IOException
|
||||
@ -15,8 +17,15 @@ object GroupUtil {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): String {
|
||||
return OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID)
|
||||
fun getEncodedOpenGroupInboxID(openGroup: OpenGroup, sessionId: SessionId): Address {
|
||||
val openGroupInboxId =
|
||||
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
|
||||
return getEncodedOpenGroupInboxID(openGroupInboxId)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address {
|
||||
return Address.fromSerialized(OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ -51,7 +60,7 @@ object GroupUtil {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDecodedOpenGroupInbox(groupID: String): String {
|
||||
fun getDecodedOpenGroupInboxSessionId(groupID: String): String {
|
||||
val decodedGroupId = getDecodedGroupID(groupID)
|
||||
if (decodedGroupId.split("!").count() > 2) {
|
||||
return decodedGroupId.split("!", limit = 3)[2]
|
||||
|
@ -37,12 +37,12 @@ interface TextSecurePreferences {
|
||||
fun setLastConfigurationSyncTime(value: Long)
|
||||
fun getConfigurationMessageSynced(): Boolean
|
||||
fun setConfigurationMessageSynced(value: Boolean)
|
||||
fun isUsingFCM(): Boolean
|
||||
fun setIsUsingFCM(value: Boolean)
|
||||
fun getFCMToken(): String?
|
||||
fun setFCMToken(value: String)
|
||||
fun getLastFCMUploadTime(): Long
|
||||
fun setLastFCMUploadTime(value: Long)
|
||||
fun isPushEnabled(): Boolean
|
||||
fun setPushEnabled(value: Boolean)
|
||||
fun getPushToken(): String?
|
||||
fun setPushToken(value: String)
|
||||
fun getPushRegisterTime(): Long
|
||||
fun setPushRegisterTime(value: Long)
|
||||
fun isScreenLockEnabled(): Boolean
|
||||
fun setScreenLockEnabled(value: Boolean)
|
||||
fun getScreenLockTimeout(): Long
|
||||
@ -251,9 +251,9 @@ interface TextSecurePreferences {
|
||||
const val LINK_PREVIEWS = "pref_link_previews"
|
||||
const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning"
|
||||
const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
|
||||
const val IS_USING_FCM = "pref_is_using_fcm"
|
||||
const val FCM_TOKEN = "pref_fcm_token"
|
||||
const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2"
|
||||
const val IS_PUSH_ENABLED = "pref_is_using_fcm"
|
||||
const val PUSH_TOKEN = "pref_fcm_token_2"
|
||||
const val PUSH_REGISTER_TIME = "pref_last_fcm_token_upload_time_2"
|
||||
const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
|
||||
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
||||
const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
|
||||
@ -287,6 +287,8 @@ interface TextSecurePreferences {
|
||||
const val OCEAN_DARK = "ocean.dark"
|
||||
const val OCEAN_LIGHT = "ocean.light"
|
||||
|
||||
const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS"
|
||||
|
||||
@JvmStatic
|
||||
fun getLastConfigurationSyncTime(context: Context): Long {
|
||||
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
|
||||
@ -309,31 +311,31 @@ interface TextSecurePreferences {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUsingFCM(context: Context): Boolean {
|
||||
return getBooleanPreference(context, IS_USING_FCM, false)
|
||||
fun isPushEnabled(context: Context): Boolean {
|
||||
return getBooleanPreference(context, IS_PUSH_ENABLED, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setIsUsingFCM(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, IS_USING_FCM, value)
|
||||
fun setPushEnabled(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, IS_PUSH_ENABLED, value)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getFCMToken(context: Context): String? {
|
||||
return getStringPreference(context, FCM_TOKEN, "")
|
||||
fun getPushToken(context: Context): String? {
|
||||
return getStringPreference(context, PUSH_TOKEN, "")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setFCMToken(context: Context, value: String) {
|
||||
setStringPreference(context, FCM_TOKEN, value)
|
||||
fun setPushToken(context: Context, value: String?) {
|
||||
setStringPreference(context, PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
fun getLastFCMUploadTime(context: Context): Long {
|
||||
return getLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, 0)
|
||||
fun getPushRegisterTime(context: Context): Long {
|
||||
return getLongPreference(context, PUSH_REGISTER_TIME, 0)
|
||||
}
|
||||
|
||||
fun setLastFCMUploadTime(context: Context, value: Long) {
|
||||
setLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, value)
|
||||
fun setPushRegisterTime(context: Context, value: Long) {
|
||||
setLongPreference(context, PUSH_REGISTER_TIME, value)
|
||||
}
|
||||
|
||||
// endregion
|
||||
@ -1008,7 +1010,6 @@ interface TextSecurePreferences {
|
||||
fun clearAll(context: Context) {
|
||||
getDefaultSharedPreferences(context).edit().clear().commit()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1033,28 +1034,28 @@ class AppTextSecurePreferences @Inject constructor(
|
||||
TextSecurePreferences._events.tryEmit(TextSecurePreferences.CONFIGURATION_SYNCED)
|
||||
}
|
||||
|
||||
override fun isUsingFCM(): Boolean {
|
||||
return getBooleanPreference(TextSecurePreferences.IS_USING_FCM, false)
|
||||
override fun isPushEnabled(): Boolean {
|
||||
return getBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, false)
|
||||
}
|
||||
|
||||
override fun setIsUsingFCM(value: Boolean) {
|
||||
setBooleanPreference(TextSecurePreferences.IS_USING_FCM, value)
|
||||
override fun setPushEnabled(value: Boolean) {
|
||||
setBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, value)
|
||||
}
|
||||
|
||||
override fun getFCMToken(): String? {
|
||||
return getStringPreference(TextSecurePreferences.FCM_TOKEN, "")
|
||||
override fun getPushToken(): String? {
|
||||
return getStringPreference(TextSecurePreferences.PUSH_TOKEN, "")
|
||||
}
|
||||
|
||||
override fun setFCMToken(value: String) {
|
||||
setStringPreference(TextSecurePreferences.FCM_TOKEN, value)
|
||||
override fun setPushToken(value: String) {
|
||||
setStringPreference(TextSecurePreferences.PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
override fun getLastFCMUploadTime(): Long {
|
||||
return getLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, 0)
|
||||
override fun getPushRegisterTime(): Long {
|
||||
return getLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, 0)
|
||||
}
|
||||
|
||||
override fun setLastFCMUploadTime(value: Long) {
|
||||
setLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, value)
|
||||
override fun setPushRegisterTime(value: Long) {
|
||||
setLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, value)
|
||||
}
|
||||
|
||||
override fun isScreenLockEnabled(): Boolean {
|
||||
|
@ -0,0 +1,169 @@
|
||||
package org.session.libsession.utilities.bencode
|
||||
|
||||
import java.util.LinkedList
|
||||
|
||||
object Bencode {
|
||||
class Decoder(source: ByteArray) {
|
||||
|
||||
private val iterator = LinkedList<Byte>().apply {
|
||||
addAll(source.asIterable())
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an element based on next marker assumed to be string/int/list/dict or return null
|
||||
*/
|
||||
fun decode(): BencodeElement? {
|
||||
val result = when (iterator.peek()?.toInt()?.toChar()) {
|
||||
in NUMBERS -> decodeString()
|
||||
INT_INDICATOR -> decodeInt()
|
||||
LIST_INDICATOR -> decodeList()
|
||||
DICT_INDICATOR -> decodeDict()
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a string element from iterator assumed to have structure `{length}:{data}`
|
||||
*/
|
||||
private fun decodeString(): BencodeString? {
|
||||
val lengthStrings = buildString {
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != SEPARATOR) {
|
||||
append(iterator.pop().toInt().toChar())
|
||||
}
|
||||
}
|
||||
iterator.pop() // drop `:`
|
||||
val length = lengthStrings.toIntOrNull(10) ?: return null
|
||||
val remaining = (0 until length).map { iterator.pop() }.toByteArray()
|
||||
return BencodeString(remaining)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an int element from iterator assumed to have structure `i{int}e`
|
||||
*/
|
||||
private fun decodeInt(): BencodeElement? {
|
||||
iterator.pop() // drop `i`
|
||||
val intString = buildString {
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
append(iterator.pop().toInt().toChar())
|
||||
}
|
||||
}
|
||||
val asInt = intString.toIntOrNull(10) ?: return null
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeInteger(asInt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a list element from iterator assumed to have structure `l{data}e`
|
||||
*/
|
||||
private fun decodeList(): BencodeElement {
|
||||
iterator.pop() // drop `l`
|
||||
val listElements = mutableListOf<BencodeElement>()
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
decode()?.let { nextElement ->
|
||||
listElements += nextElement
|
||||
}
|
||||
}
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeList(listElements)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a dict element from iterator assumed to have structure `d{data}e`
|
||||
*/
|
||||
private fun decodeDict(): BencodeElement? {
|
||||
iterator.pop() // drop `d`
|
||||
val dictElements = mutableMapOf<String,BencodeElement>()
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
val key = decodeString() ?: return null
|
||||
val value = decode() ?: return null
|
||||
dictElements += key.value.decodeToString() to value
|
||||
}
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeDict(dictElements)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NUMBERS = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
|
||||
private const val INT_INDICATOR = 'i'
|
||||
private const val LIST_INDICATOR = 'l'
|
||||
private const val DICT_INDICATOR = 'd'
|
||||
private const val END_INDICATOR = 'e'
|
||||
private const val SEPARATOR = ':'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class BencodeElement {
|
||||
abstract fun encode(): ByteArray
|
||||
}
|
||||
|
||||
fun String.bencode() = BencodeString(this.encodeToByteArray())
|
||||
fun Int.bencode() = BencodeInteger(this)
|
||||
|
||||
data class BencodeString(val value: ByteArray): BencodeElement() {
|
||||
override fun encode(): ByteArray = buildString {
|
||||
append(value.size.toString())
|
||||
append(':')
|
||||
}.toByteArray() + value
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BencodeString
|
||||
|
||||
if (!value.contentEquals(other.value)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.contentHashCode()
|
||||
}
|
||||
}
|
||||
data class BencodeInteger(val value: Int): BencodeElement() {
|
||||
override fun encode(): ByteArray = buildString {
|
||||
append('i')
|
||||
append(value.toString())
|
||||
append('e')
|
||||
}.toByteArray()
|
||||
}
|
||||
data class BencodeList(val values: List<BencodeElement>): BencodeElement() {
|
||||
|
||||
constructor(vararg values: BencodeElement) : this(values.toList())
|
||||
|
||||
override fun encode(): ByteArray = "l".toByteArray() +
|
||||
values.fold(byteArrayOf()) { array, element -> array + element.encode() } +
|
||||
"e".toByteArray()
|
||||
}
|
||||
data class BencodeDict(val values: Map<String, BencodeElement>): BencodeElement() {
|
||||
|
||||
constructor(vararg values: Pair<String, BencodeElement>) : this(values.toMap())
|
||||
|
||||
override fun encode(): ByteArray = "d".toByteArray() +
|
||||
values.entries.fold(byteArrayOf()) { array, (key, value) ->
|
||||
array + key.bencode().encode() + value.encode()
|
||||
} + "e".toByteArray()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BencodeDict
|
||||
|
||||
if (values != other.values) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return values.hashCode()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -101,6 +101,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private String notificationChannel;
|
||||
private boolean forceSmsSelection;
|
||||
private String wrapperHash;
|
||||
private boolean blocksCommunityMessageRequests;
|
||||
|
||||
private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED;
|
||||
|
||||
@ -194,6 +195,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.unidentifiedAccessMode = details.get().unidentifiedAccessMode;
|
||||
this.forceSmsSelection = details.get().forceSmsSelection;
|
||||
this.notifyType = details.get().notifyType;
|
||||
this.blocksCommunityMessageRequests = details.get().blocksCommunityMessageRequests;
|
||||
this.disappearingState = details.get().disappearingState;
|
||||
|
||||
this.participants.clear();
|
||||
@ -232,6 +234,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
Recipient.this.forceSmsSelection = result.forceSmsSelection;
|
||||
Recipient.this.notifyType = result.notifyType;
|
||||
Recipient.this.disappearingState = result.disappearingState;
|
||||
Recipient.this.blocksCommunityMessageRequests = result.blocksCommunityMessageRequests;
|
||||
|
||||
Recipient.this.participants.clear();
|
||||
Recipient.this.participants.addAll(result.participants);
|
||||
@ -285,6 +288,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
|
||||
this.forceSmsSelection = details.forceSmsSelection;
|
||||
this.wrapperHash = details.wrapperHash;
|
||||
this.blocksCommunityMessageRequests = details.blocksCommunityMessageRequests;
|
||||
|
||||
this.participants.addAll(details.participants);
|
||||
this.resolving = false;
|
||||
@ -325,7 +329,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return this.name;
|
||||
}
|
||||
} else if (isOpenGroupInboxRecipient()){
|
||||
String inboxID = GroupUtil.getDecodedOpenGroupInbox(sessionID);
|
||||
String inboxID = GroupUtil.getDecodedOpenGroupInboxSessionId(sessionID);
|
||||
Contact contact = storage.getContactWithSessionID(inboxID);
|
||||
if (contact == null) { return sessionID; }
|
||||
return contact.displayName(Contact.ContactContext.REGULAR);
|
||||
@ -349,6 +353,18 @@ public class Recipient implements RecipientModifiedListener {
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
public boolean getBlocksCommunityMessageRequests() {
|
||||
return blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
public void setBlocksCommunityMessageRequests(boolean blocksCommunityMessageRequests) {
|
||||
synchronized (this) {
|
||||
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public synchronized @NonNull MaterialColor getColor() {
|
||||
if (isGroupRecipient()) return MaterialColor.GROUP;
|
||||
else if (color != null) return color;
|
||||
@ -779,12 +795,43 @@ public class Recipient implements RecipientModifiedListener {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Recipient recipient = (Recipient) o;
|
||||
return resolving == recipient.resolving && mutedUntil == recipient.mutedUntil && notifyType == recipient.notifyType && blocked == recipient.blocked && approved == recipient.approved && approvedMe == recipient.approvedMe && expireMessages == recipient.expireMessages && address.equals(recipient.address) && Objects.equals(name, recipient.name) && Objects.equals(customLabel, recipient.customLabel) && Objects.equals(groupAvatarId, recipient.groupAvatarId) && Arrays.equals(profileKey, recipient.profileKey) && Objects.equals(profileName, recipient.profileName) && Objects.equals(profileAvatar, recipient.profileAvatar) && Objects.equals(wrapperHash, recipient.wrapperHash);
|
||||
return resolving == recipient.resolving
|
||||
&& mutedUntil == recipient.mutedUntil
|
||||
&& notifyType == recipient.notifyType
|
||||
&& blocked == recipient.blocked
|
||||
&& approved == recipient.approved
|
||||
&& approvedMe == recipient.approvedMe
|
||||
&& expireMessages == recipient.expireMessages
|
||||
&& address.equals(recipient.address)
|
||||
&& Objects.equals(name, recipient.name)
|
||||
&& Objects.equals(customLabel, recipient.customLabel)
|
||||
&& Objects.equals(groupAvatarId, recipient.groupAvatarId)
|
||||
&& Arrays.equals(profileKey, recipient.profileKey)
|
||||
&& Objects.equals(profileName, recipient.profileName)
|
||||
&& Objects.equals(profileAvatar, recipient.profileAvatar)
|
||||
&& Objects.equals(wrapperHash, recipient.wrapperHash)
|
||||
&& blocksCommunityMessageRequests == recipient.blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar, wrapperHash);
|
||||
int result = Objects.hash(
|
||||
address,
|
||||
name,
|
||||
customLabel,
|
||||
resolving,
|
||||
groupAvatarId,
|
||||
mutedUntil,
|
||||
notifyType,
|
||||
blocked,
|
||||
approved,
|
||||
approvedMe,
|
||||
expireMessages,
|
||||
profileName,
|
||||
profileAvatar,
|
||||
wrapperHash,
|
||||
blocksCommunityMessageRequests
|
||||
);
|
||||
result = 31 * result + Arrays.hashCode(profileKey);
|
||||
return result;
|
||||
}
|
||||
@ -908,6 +955,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
private final String wrapperHash;
|
||||
private final boolean blocksCommunityMessageRequests;
|
||||
|
||||
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
|
||||
int notifyType,
|
||||
@ -931,7 +979,9 @@ public class Recipient implements RecipientModifiedListener {
|
||||
@Nullable String notificationChannel,
|
||||
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
|
||||
boolean forceSmsSelection,
|
||||
String wrapperHash)
|
||||
String wrapperHash,
|
||||
boolean blocksCommunityMessageRequests
|
||||
)
|
||||
{
|
||||
this.blocked = blocked;
|
||||
this.approved = approved;
|
||||
@ -959,6 +1009,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||
this.forceSmsSelection = forceSmsSelection;
|
||||
this.wrapperHash = wrapperHash;
|
||||
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
public @Nullable MaterialColor getColor() {
|
||||
@ -1065,6 +1116,10 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return wrapperHash;
|
||||
}
|
||||
|
||||
public boolean getBlocksCommunityMessageRequests() {
|
||||
return blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -180,6 +180,7 @@ class RecipientProvider {
|
||||
@NonNull final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
final boolean forceSmsSelection;
|
||||
final String wrapperHash;
|
||||
final boolean blocksCommunityMessageRequests;
|
||||
|
||||
RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId,
|
||||
boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings,
|
||||
@ -214,6 +215,7 @@ class RecipientProvider {
|
||||
this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED;
|
||||
this.forceSmsSelection = settings != null && settings.isForceSmsSelection();
|
||||
this.wrapperHash = settings != null ? settings.getWrapperHash() : null;
|
||||
this.blocksCommunityMessageRequests = settings != null && settings.getBlocksCommunityMessageRequests();
|
||||
|
||||
if (name == null && settings != null) this.name = settings.getSystemDisplayName();
|
||||
else this.name = name;
|
||||
|
@ -0,0 +1,107 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.session.libsession.utilities.bencode.Bencode
|
||||
import org.session.libsession.utilities.bencode.BencodeDict
|
||||
import org.session.libsession.utilities.bencode.BencodeInteger
|
||||
import org.session.libsession.utilities.bencode.BencodeList
|
||||
import org.session.libsession.utilities.bencode.bencode
|
||||
|
||||
class BencoderTest {
|
||||
|
||||
@Test
|
||||
fun `it should decode a basic string`() {
|
||||
val basicString = "5:howdy".toByteArray()
|
||||
val bencoder = Bencode.Decoder(basicString)
|
||||
val result = bencoder.decode()
|
||||
assertEquals("howdy".bencode(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should decode a basic integer`() {
|
||||
val basicInteger = "i3e".toByteArray()
|
||||
val bencoder = Bencode.Decoder(basicInteger)
|
||||
val result = bencoder.decode()
|
||||
assertEquals(BencodeInteger(3), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should decode a list of integers`() {
|
||||
val basicIntList = "li1ei2ee".toByteArray()
|
||||
val bencoder = Bencode.Decoder(basicIntList)
|
||||
val result = bencoder.decode()
|
||||
assertEquals(
|
||||
BencodeList(
|
||||
1.bencode(),
|
||||
2.bencode()
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should decode a basic dict`() {
|
||||
val basicDict = "d4:spaml1:a1:bee".toByteArray()
|
||||
val bencoder = Bencode.Decoder(basicDict)
|
||||
val result = bencoder.decode()
|
||||
assertEquals(
|
||||
BencodeDict(
|
||||
"spam" to BencodeList(
|
||||
"a".bencode(),
|
||||
"b".bencode()
|
||||
)
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a basic string`() {
|
||||
val basicString = "5:howdy".toByteArray()
|
||||
val element = "howdy".bencode()
|
||||
assertArrayEquals(basicString, element.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a basic int`() {
|
||||
val basicInt = "i3e".toByteArray()
|
||||
val element = 3.bencode()
|
||||
assertArrayEquals(basicInt, element.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a basic list`() {
|
||||
val basicList = "li1ei2ee".toByteArray()
|
||||
val element = BencodeList(1.bencode(),2.bencode())
|
||||
assertArrayEquals(basicList, element.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a basic dict`() {
|
||||
val basicDict = "d4:spaml1:a1:bee".toByteArray()
|
||||
val element = BencodeDict(
|
||||
"spam" to BencodeList(
|
||||
"a".bencode(),
|
||||
"b".bencode()
|
||||
)
|
||||
)
|
||||
assertArrayEquals(basicDict, element.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a more complex real world case`() {
|
||||
val source = "d15:lastReadMessaged66:031122334455667788990011223344556677889900112233445566778899001122i1234568790e66:051122334455667788990011223344556677889900112233445566778899001122i1234568790ee5:seqNoi1ee".toByteArray()
|
||||
val result = Bencode.Decoder(source).decode()
|
||||
val expected = BencodeDict(
|
||||
"lastReadMessage" to BencodeDict(
|
||||
"051122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode(),
|
||||
"031122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode()
|
||||
),
|
||||
"seqNo" to BencodeInteger(1)
|
||||
)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user