mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 02:55:23 +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
|
ffpr
|
||||||
*.sh
|
*.sh
|
||||||
pkcs11.password
|
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.
|
6. Project initialization and building should proceed.
|
||||||
7. Clone submodules with `git submodule update --init --recursive`
|
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
|
Contributing code
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
311
app/build.gradle
311
app/build.gradle
@ -24,15 +24,181 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'witness'
|
apply plugin: 'witness'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-parcelize'
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
apply plugin: 'dagger.hilt.android.plugin'
|
apply plugin: 'dagger.hilt.android.plugin'
|
||||||
|
|
||||||
|
|
||||||
configurations.all {
|
configurations.all {
|
||||||
exclude module: "commons-logging"
|
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 {
|
dependencies {
|
||||||
|
|
||||||
implementation("com.google.dagger:hilt-android:2.46.1")
|
implementation("com.google.dagger:hilt-android:2.46.1")
|
||||||
@ -59,11 +225,12 @@ dependencies {
|
|||||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||||
implementation "androidx.core:core-ktx:$coreVersion"
|
implementation "androidx.core:core-ktx:$coreVersion"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
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-core'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
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-core:2.9.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||||
@ -176,144 +343,6 @@ dependencies {
|
|||||||
implementation 'androidx.compose.material:material:1.4.3'
|
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() {
|
static def getLastCommitTimestamp() {
|
||||||
new ByteArrayOutputStream().withStream { os ->
|
new ByteArrayOutputStream().withStream { os ->
|
||||||
return os.toString() + "000"
|
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:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||||
</activity>
|
</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"
|
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
|
@ -41,6 +41,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
|||||||
import org.session.libsession.snode.SnodeModule;
|
import org.session.libsession.snode.SnodeModule;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
|
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
|
||||||
|
import org.session.libsession.utilities.Device;
|
||||||
import org.session.libsession.utilities.ProfilePictureUtilities;
|
import org.session.libsession.utilities.ProfilePictureUtilities;
|
||||||
import org.session.libsession.utilities.SSKEnvironment;
|
import org.session.libsession.utilities.SSKEnvironment;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
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.logging.UncaughtExceptionLogger;
|
||||||
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
|
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
|
||||||
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
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.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
||||||
|
import org.thoughtcrime.securesms.notifications.PushRegistry;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
@ -143,8 +143,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
|
|
||||||
@Inject LokiAPIDatabase lokiAPIDatabase;
|
@Inject LokiAPIDatabase lokiAPIDatabase;
|
||||||
@Inject public Storage storage;
|
@Inject public Storage storage;
|
||||||
|
@Inject Device device;
|
||||||
@Inject MessageDataProvider messageDataProvider;
|
@Inject MessageDataProvider messageDataProvider;
|
||||||
@Inject TextSecurePreferences textSecurePreferences;
|
@Inject TextSecurePreferences textSecurePreferences;
|
||||||
|
@Inject PushRegistry pushRegistry;
|
||||||
@Inject ConfigFactory configFactory;
|
@Inject ConfigFactory configFactory;
|
||||||
CallMessageProcessor callMessageProcessor;
|
CallMessageProcessor callMessageProcessor;
|
||||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||||
@ -207,8 +209,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
DatabaseModule.init(this);
|
DatabaseModule.init(this);
|
||||||
MessagingModuleConfiguration.configure(this);
|
MessagingModuleConfiguration.configure(this);
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
|
messagingModuleConfiguration = new MessagingModuleConfiguration(
|
||||||
|
this,
|
||||||
storage,
|
storage,
|
||||||
|
device,
|
||||||
messageDataProvider,
|
messageDataProvider,
|
||||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
||||||
configFactory
|
configFactory
|
||||||
@ -226,10 +230,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
broadcaster = new Broadcaster(this);
|
broadcaster = new Broadcaster(this);
|
||||||
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
||||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
|
||||||
if (userPublicKey != null) {
|
|
||||||
registerForFCMIfNeeded(false);
|
|
||||||
}
|
|
||||||
initializeExpiringMessageManager();
|
initializeExpiringMessageManager();
|
||||||
initializeTypingStatusRepository();
|
initializeTypingStatusRepository();
|
||||||
initializeTypingStatusSender();
|
initializeTypingStatusSender();
|
||||||
@ -427,33 +427,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class ProviderInitializationException extends RuntimeException { }
|
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() {
|
private void setUpPollingIfNeeded() {
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||||
if (userPublicKey == null) return;
|
if (userPublicKey == null) return;
|
||||||
@ -524,18 +497,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||||
String token = TextSecurePreferences.getFCMToken(this);
|
|
||||||
if (token != null && !token.isEmpty()) {
|
|
||||||
LokiPushNotificationManager.unregister(token, this);
|
|
||||||
}
|
|
||||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
|
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
|
||||||
firebaseInstanceIdJob.cancel(null);
|
firebaseInstanceIdJob.cancel(null);
|
||||||
}
|
}
|
||||||
String displayName = TextSecurePreferences.getProfileName(this);
|
String displayName = TextSecurePreferences.getProfileName(this);
|
||||||
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
boolean isUsingFCM = TextSecurePreferences.isPushEnabled(this);
|
||||||
TextSecurePreferences.clearAll(this);
|
TextSecurePreferences.clearAll(this);
|
||||||
if (isMigratingToV2KeyPair) {
|
if (isMigratingToV2KeyPair) {
|
||||||
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
TextSecurePreferences.setPushEnabled(this, isUsingFCM);
|
||||||
TextSecurePreferences.setProfileName(this, displayName);
|
TextSecurePreferences.setProfileName(this, displayName);
|
||||||
}
|
}
|
||||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
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,
|
text,
|
||||||
contentDescription,
|
contentDescription,
|
||||||
R.style.Widget_Session_Button_Dialog_DestructiveText,
|
R.style.Widget_Session_Button_Dialog_DestructiveText,
|
||||||
listener
|
) { listener() }
|
||||||
)
|
|
||||||
|
|
||||||
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok, 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 = listener)
|
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button) { listener() }
|
||||||
|
|
||||||
fun button(
|
fun button(
|
||||||
@StringRes text: Int,
|
@StringRes text: Int,
|
||||||
@StringRes contentDescriptionRes: Int = text,
|
@StringRes contentDescriptionRes: Int = text,
|
||||||
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
|
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
|
||||||
|
dismiss: Boolean = true,
|
||||||
listener: (() -> Unit) = {}
|
listener: (() -> Unit) = {}
|
||||||
) = Button(context, null, 0, style).apply {
|
) = Button(context, null, 0, style).apply {
|
||||||
setText(text)
|
setText(text)
|
||||||
@ -129,7 +129,7 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
.apply { setMargins(toPx(20, resources)) }
|
.apply { setMargins(toPx(20, resources)) }
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
listener.invoke()
|
listener.invoke()
|
||||||
dismiss()
|
if (dismiss) dismiss()
|
||||||
}
|
}
|
||||||
}.let(buttonLayout::addView)
|
}.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 com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
||||||
import network.loki.messenger.databinding.ViewUserBinding
|
|
||||||
import org.session.libsession.avatars.ContactColors
|
import org.session.libsession.avatars.ContactColors
|
||||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||||
import org.session.libsession.avatars.ProfileContactPhoto
|
import org.session.libsession.avatars.ProfileContactPhoto
|
||||||
@ -74,7 +73,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
additionalDisplayName = getUserDisplayName(apk)
|
additionalDisplayName = getUserDisplayName(apk)
|
||||||
}
|
}
|
||||||
} else if(recipient.isOpenGroupInboxRecipient) {
|
} else if(recipient.isOpenGroupInboxRecipient) {
|
||||||
val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize())
|
val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
|
||||||
this.publicKey = publicKey
|
this.publicKey = publicKey
|
||||||
displayName = getUserDisplayName(publicKey)
|
displayName = getUserDisplayName(publicKey)
|
||||||
additionalPublicKey = null
|
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 android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@ -24,7 +24,7 @@ public final class ContactUtil {
|
|||||||
return SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message));
|
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) {
|
if (contact == null) {
|
||||||
return "";
|
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.set
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.view.drawToBitmap
|
import androidx.core.view.drawToBitmap
|
||||||
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.Lifecycle
|
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.Reaction
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
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.MessageSender
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
@ -105,7 +105,6 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
|||||||
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
||||||
import org.thoughtcrime.securesms.audio.AudioRecorder
|
import org.thoughtcrime.securesms.audio.AudioRecorder
|
||||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
|
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.ConversationActionBarDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.expiration.ExpirationSettingsActivity
|
import org.thoughtcrime.securesms.conversation.expiration.ExpirationSettingsActivity
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
|
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.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||||
|
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
||||||
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
@ -237,11 +237,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
|
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
|
||||||
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
|
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
|
||||||
fromSerialized(it)
|
fromSerialized(it)
|
||||||
} ?: run {
|
} ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, sessionId)
|
||||||
val openGroupInboxId =
|
|
||||||
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
|
|
||||||
fromSerialized(GroupUtil.getEncodedOpenGroupInboxID(openGroupInboxId))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
@ -250,7 +246,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
} ?: finish()
|
} ?: finish()
|
||||||
}
|
}
|
||||||
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair(), contentResolver)
|
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair())
|
||||||
}
|
}
|
||||||
private var actionMode: ActionMode? = null
|
private var actionMode: ActionMode? = null
|
||||||
private var unreadCount = 0
|
private var unreadCount = 0
|
||||||
@ -309,8 +305,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
handleSwipeToReply(message)
|
handleSwipeToReply(message)
|
||||||
},
|
},
|
||||||
onItemLongPress = { message, position, view ->
|
onItemLongPress = { message, position, view ->
|
||||||
if (!isMessageRequestThread() &&
|
if (!viewModel.isMessageRequestThread &&
|
||||||
(viewModel.openGroup == null || Capability.REACTIONS.name.lowercase() in viewModel.serverCapabilities)
|
viewModel.canReactToMessages
|
||||||
) {
|
) {
|
||||||
showEmojiPicker(message, view)
|
showEmojiPicker(message, view)
|
||||||
} else {
|
} else {
|
||||||
@ -593,26 +589,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
// called from onCreate
|
// called from onCreate
|
||||||
private fun setUpInputBar() {
|
private fun setUpInputBar() {
|
||||||
binding!!.inputBar.isVisible = viewModel.openGroup == null || viewModel.openGroup?.canWrite == true
|
val binding = binding ?: return
|
||||||
binding!!.inputBar.delegate = this
|
binding.inputBar.isGone = viewModel.hidesInputBar()
|
||||||
binding!!.inputBarRecordingView.delegate = this
|
binding.inputBar.delegate = this
|
||||||
|
binding.inputBarRecordingView.delegate = this
|
||||||
// GIF button
|
// GIF button
|
||||||
binding!!.gifButtonContainer.addView(gifButton)
|
binding.gifButtonContainer.addView(gifButton)
|
||||||
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
gifButton.onUp = { showGIFPicker() }
|
gifButton.onUp = { showGIFPicker() }
|
||||||
gifButton.snIsEnabled = false
|
gifButton.snIsEnabled = false
|
||||||
// Document button
|
// Document button
|
||||||
binding!!.documentButtonContainer.addView(documentButton)
|
binding.documentButtonContainer.addView(documentButton)
|
||||||
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
documentButton.onUp = { showDocumentPicker() }
|
documentButton.onUp = { showDocumentPicker() }
|
||||||
documentButton.snIsEnabled = false
|
documentButton.snIsEnabled = false
|
||||||
// Library button
|
// Library button
|
||||||
binding!!.libraryButtonContainer.addView(libraryButton)
|
binding.libraryButtonContainer.addView(libraryButton)
|
||||||
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
libraryButton.onUp = { pickFromLibrary() }
|
libraryButton.onUp = { pickFromLibrary() }
|
||||||
libraryButton.snIsEnabled = false
|
libraryButton.snIsEnabled = false
|
||||||
// Camera button
|
// Camera button
|
||||||
binding!!.cameraButtonContainer.addView(cameraButton)
|
binding.cameraButtonContainer.addView(cameraButton)
|
||||||
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
cameraButton.onUp = { showCamera() }
|
cameraButton.onUp = { showCamera() }
|
||||||
cameraButton.snIsEnabled = false
|
cameraButton.snIsEnabled = false
|
||||||
@ -776,7 +773,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||||
val recipient = viewModel.recipient ?: return false
|
val recipient = viewModel.recipient ?: return false
|
||||||
if (!isMessageRequestThread()) {
|
if (!viewModel.isMessageRequestThread) {
|
||||||
ConversationMenuHelper.onPrepareOptionsMenu(
|
ConversationMenuHelper.onPrepareOptionsMenu(
|
||||||
menu,
|
menu,
|
||||||
menuInflater,
|
menuInflater,
|
||||||
@ -1080,11 +1077,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private fun updatePlaceholder() {
|
private fun updatePlaceholder() {
|
||||||
val recipient = viewModel.recipient
|
val recipient = viewModel.recipient
|
||||||
?: return Log.w("Loki", "recipient was null in placeholder update")
|
?: return Log.w("Loki", "recipient was null in placeholder update")
|
||||||
|
val blindedRecipient = viewModel.blindedRecipient
|
||||||
val binding = binding ?: return
|
val binding = binding ?: return
|
||||||
val openGroup = viewModel.openGroup
|
val openGroup = viewModel.openGroup
|
||||||
val (textResource, insertParam) = when {
|
val (textResource, insertParam) = when {
|
||||||
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
|
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()
|
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()
|
else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
|
||||||
}
|
}
|
||||||
val showPlaceholder = adapter.itemCount == 0
|
val showPlaceholder = adapter.itemCount == 0
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.cash.copper.flow.observeQuery
|
|
||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
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.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
@ -31,7 +28,6 @@ import java.util.UUID
|
|||||||
class ConversationViewModel(
|
class ConversationViewModel(
|
||||||
val threadId: Long,
|
val threadId: Long,
|
||||||
val edKeyPair: KeyPair?,
|
val edKeyPair: KeyPair?,
|
||||||
private val contentResolver: ContentResolver,
|
|
||||||
private val repository: ConversationRepository,
|
private val repository: ConversationRepository,
|
||||||
private val storage: Storage
|
private val storage: Storage
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@ -51,6 +47,15 @@ class ConversationViewModel(
|
|||||||
val recipient: Recipient?
|
val recipient: Recipient?
|
||||||
get() = _recipient.value
|
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 {
|
private var _openGroup: RetrieveOnce<OpenGroup> = RetrieveOnce {
|
||||||
storage.getOpenGroup(threadId)
|
storage.getOpenGroup(threadId)
|
||||||
}
|
}
|
||||||
@ -66,12 +71,22 @@ class ConversationViewModel(
|
|||||||
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
|
?.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 {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId))
|
repository.recipientUpdateFlow(threadId)
|
||||||
.collect {
|
.collect { recipient ->
|
||||||
val recipientExists = storage.getRecipientForThread(threadId) != null
|
if (recipient == null && _uiState.value.conversationExists) {
|
||||||
if (!recipientExists && _uiState.value.conversationExists) {
|
|
||||||
_uiState.update { it.copy(conversationExists = false) }
|
_uiState.update { it.copy(conversationExists = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,22 +218,25 @@ class ConversationViewModel(
|
|||||||
_recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId))
|
_recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hidesInputBar(): Boolean = openGroup?.canWrite != true &&
|
||||||
|
blindedRecipient?.blocksCommunityMessageRequests == true
|
||||||
|
|
||||||
|
|
||||||
@dagger.assisted.AssistedFactory
|
@dagger.assisted.AssistedFactory
|
||||||
interface AssistedFactory {
|
interface AssistedFactory {
|
||||||
fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory
|
fun create(threadId: Long, edKeyPair: KeyPair?): Factory
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
class Factory @AssistedInject constructor(
|
class Factory @AssistedInject constructor(
|
||||||
@Assisted private val threadId: Long,
|
@Assisted private val threadId: Long,
|
||||||
@Assisted private val edKeyPair: KeyPair?,
|
@Assisted private val edKeyPair: KeyPair?,
|
||||||
@Assisted private val contentResolver: ContentResolver,
|
|
||||||
private val repository: ConversationRepository,
|
private val repository: ConversationRepository,
|
||||||
private val storage: Storage
|
private val storage: Storage
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
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 IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
|
||||||
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
|
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 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 LOKI_SEED = "loki_seed";
|
||||||
public static final String HAS_MIGRATED_KEY = "has_migrated_keys";
|
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 FORCE_SMS_SELECTION = "force_sms_selection";
|
||||||
private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none
|
private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none
|
||||||
private static final String WRAPPER_HASH = "wrapper_hash";
|
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[] {
|
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,
|
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,
|
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
|
||||||
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||||
UNIDENTIFIED_ACCESS_MODE,
|
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)
|
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;";
|
"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_ALL = 0;
|
||||||
public static final int NOTIFY_TYPE_MENTIONS = 1;
|
public static final int NOTIFY_TYPE_MENTIONS = 1;
|
||||||
public static final int NOTIFY_TYPE_NONE = 2;
|
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));
|
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
|
||||||
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
|
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
|
||||||
String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH));
|
String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH));
|
||||||
|
boolean blocksCommunityMessageRequests = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKS_COMMUNITY_MESSAGE_REQUESTS)) == 1;
|
||||||
|
|
||||||
MaterialColor color;
|
MaterialColor color;
|
||||||
byte[] profileKey = null;
|
byte[] profileKey = null;
|
||||||
@ -236,7 +243,7 @@ public class RecipientDatabase extends Database {
|
|||||||
systemPhoneLabel, systemContactUri,
|
systemPhoneLabel, systemContactUri,
|
||||||
signalProfileName, signalProfileAvatar, profileSharing,
|
signalProfileName, signalProfileAvatar, profileSharing,
|
||||||
notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||||
forceSmsSelection, wrapperHash));
|
forceSmsSelection, wrapperHash, blocksCommunityMessageRequests));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) {
|
public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) {
|
||||||
@ -393,6 +400,14 @@ public class RecipientDatabase extends Database {
|
|||||||
notifyRecipientListeners();
|
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) {
|
private void updateOrInsert(Address address, ContentValues contentValues) {
|
||||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
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.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
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.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.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
@ -92,8 +92,11 @@ import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
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,
|
open class Storage(
|
||||||
ThreadDatabase.ConversationThreadUpdateListener {
|
context: Context,
|
||||||
|
helper: SQLCipherOpenHelper,
|
||||||
|
private val configFactory: ConfigFactory
|
||||||
|
) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener {
|
||||||
|
|
||||||
override fun threadCreated(address: Address, threadId: Long) {
|
override fun threadCreated(address: Address, threadId: Long) {
|
||||||
val localUserAddress = getUserPublicKey() ?: return
|
val localUserAddress = getUserPublicKey() ?: return
|
||||||
@ -191,6 +194,11 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
|||||||
db.setProfileKey(recipient, newProfileKey)
|
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?) {
|
override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) {
|
||||||
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
|
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
|
||||||
Recipient.from(context, it, false)
|
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)
|
return configFactory.canPerformChange(variant, publicKey, changeTimestampMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isCheckingCommunityRequests(): Boolean {
|
||||||
|
return configFactory.user?.getCommunityMessageRequests() == true
|
||||||
|
}
|
||||||
|
|
||||||
private fun notifyUpdates(forConfigObject: ConfigBase, messageTimestamp: Long) {
|
private fun notifyUpdates(forConfigObject: ConfigBase, messageTimestamp: Long) {
|
||||||
when (forConfigObject) {
|
when (forConfigObject) {
|
||||||
is UserProfile -> updateUser(forConfigObject, messageTimestamp)
|
is UserProfile -> updateUser(forConfigObject, messageTimestamp)
|
||||||
@ -618,7 +630,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
|||||||
val expireTimer = group.disappearingTimer
|
val expireTimer = group.disappearingTimer
|
||||||
setExpirationTimer(groupId, expireTimer.toInt())
|
setExpirationTimer(groupId, expireTimer.toInt())
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, group.sessionId, localUserPublicKey)
|
PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey)
|
||||||
// Notify the user
|
// Notify the user
|
||||||
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||||
threadDb.setDate(threadID, formationTimestamp)
|
threadDb.setDate(threadID, formationTimestamp)
|
||||||
@ -1486,7 +1498,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
|||||||
val blindedId = when {
|
val blindedId = when {
|
||||||
recipient.isGroupRecipient -> null
|
recipient.isGroupRecipient -> null
|
||||||
recipient.isOpenGroupInboxRecipient -> {
|
recipient.isOpenGroupInboxRecipient -> {
|
||||||
GroupUtil.getDecodedOpenGroupInbox(address)
|
GroupUtil.getDecodedOpenGroupInboxSessionId(address)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (SessionId(address).prefix == IdPrefix.BLINDED) {
|
if (SessionId(address).prefix == IdPrefix.BLINDED) {
|
||||||
@ -1614,18 +1626,14 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
|||||||
if (mapping.sessionId != null) {
|
if (mapping.sessionId != null) {
|
||||||
return mapping
|
return mapping
|
||||||
}
|
}
|
||||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
getAllContacts().forEach { contact ->
|
||||||
threadDb.readerFor(threadDb.conversationList).use { reader ->
|
val sessionId = SessionId(contact.sessionID)
|
||||||
while (reader.next != null) {
|
if (sessionId.prefix == IdPrefix.STANDARD && SodiumUtilities.sessionId(sessionId.hexString, blindedId, serverPublicKey)) {
|
||||||
val recipient = reader.current.recipient
|
val contactMapping = mapping.copy(sessionId = sessionId.hexString)
|
||||||
val sessionId = recipient.address.serialize()
|
|
||||||
if (!recipient.isGroupRecipient && SodiumUtilities.sessionId(sessionId, blindedId, serverPublicKey)) {
|
|
||||||
val contactMapping = mapping.copy(sessionId = sessionId)
|
|
||||||
db.addBlindedIdMapping(contactMapping)
|
db.addBlindedIdMapping(contactMapping)
|
||||||
return contactMapping
|
return contactMapping
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
db.getBlindedIdMappingsExceptFor(server).forEach {
|
db.getBlindedIdMappingsExceptFor(server).forEach {
|
||||||
if (SodiumUtilities.sessionId(it.sessionId!!, blindedId, serverPublicKey)) {
|
if (SodiumUtilities.sessionId(it.sessionId!!, blindedId, serverPublicKey)) {
|
||||||
val otherMapping = mapping.copy(sessionId = it.sessionId)
|
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.Pair;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
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.MessagingDatabase.MarkedMessageInfo;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
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 lokiV41 = 62;
|
||||||
private static final int lokiV42 = 63;
|
private static final int lokiV42 = 63;
|
||||||
private static final int lokiV43 = 64;
|
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
|
// 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 int MIN_DATABASE_VERSION = lokiV7;
|
||||||
private static final String CIPHER3_DATABASE_NAME = "signal.db";
|
private static final String CIPHER3_DATABASE_NAME = "signal.db";
|
||||||
public static final String DATABASE_NAME = "signal_v4.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);
|
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
|
||||||
db.execSQL(RecipientDatabase.getAddWrapperHash());
|
db.execSQL(RecipientDatabase.getAddWrapperHash());
|
||||||
|
db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -603,6 +605,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (oldVersion < lokiV43) {
|
if (oldVersion < lokiV43) {
|
||||||
|
db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldVersion < lokiV44) {
|
||||||
db.execSQL(RecipientDatabase.getCreateDisappearingStateCommand());
|
db.execSQL(RecipientDatabase.getCreateDisappearingStateCommand());
|
||||||
db.execSQL(ExpirationConfigurationDatabase.CREATE_EXPIRATION_CONFIGURATION_TABLE_COMMAND);
|
db.execSQL(ExpirationConfigurationDatabase.CREATE_EXPIRATION_CONFIGURATION_TABLE_COMMAND);
|
||||||
db.execSQL(ExpirationConfigurationDatabase.MIGRATE_GROUP_CONVERSATION_EXPIRY_TYPE_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 android.content.Context
|
||||||
import network.loki.messenger.libsession_util.ConfigBase
|
import network.loki.messenger.libsession_util.ConfigBase
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
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.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
@ -24,7 +24,7 @@ object ClosedGroupManager {
|
|||||||
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
PushRegistryV1.unsubscribeGroup(closedGroupPublicKey = groupPublicKey, publicKey = userPublicKey)
|
||||||
// Stop polling
|
// Stop polling
|
||||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||||
storage.cancelPendingMessageSendJobs(threadId)
|
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.MessageSender
|
||||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.Device
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.contacts.SelectContactsAdapter
|
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.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.util.fadeIn
|
import org.thoughtcrime.securesms.util.fadeIn
|
||||||
import org.thoughtcrime.securesms.util.fadeOut
|
import org.thoughtcrime.securesms.util.fadeOut
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class CreateGroupFragment : Fragment() {
|
class CreateGroupFragment : Fragment() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var device: Device
|
||||||
|
|
||||||
private lateinit var binding: FragmentCreateGroupBinding
|
private lateinit var binding: FragmentCreateGroupBinding
|
||||||
private val viewModel: CreateGroupViewModel by viewModels()
|
private val viewModel: CreateGroupViewModel by viewModels()
|
||||||
|
|
||||||
@ -86,7 +91,7 @@ class CreateGroupFragment : Fragment() {
|
|||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||||
isLoading = true
|
isLoading = true
|
||||||
binding.loaderContainer.fadeIn()
|
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()
|
binding.loaderContainer.fadeOut()
|
||||||
isLoading = false
|
isLoading = false
|
||||||
val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(requireContext(), Address.fromSerialized(groupID), 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.messagerequests.MessageRequestsActivity
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||||
import org.thoughtcrime.securesms.onboarding.SeedActivity
|
import org.thoughtcrime.securesms.onboarding.SeedActivity
|
||||||
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
@ -106,6 +107,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
@Inject lateinit var groupDatabase: GroupDatabase
|
@Inject lateinit var groupDatabase: GroupDatabase
|
||||||
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
||||||
@Inject lateinit var configFactory: ConfigFactory
|
@Inject lateinit var configFactory: ConfigFactory
|
||||||
|
@Inject lateinit var pushRegistry: PushRegistry
|
||||||
|
|
||||||
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
||||||
private val homeViewModel by viewModels<HomeViewModel>()
|
private val homeViewModel by viewModels<HomeViewModel>()
|
||||||
@ -230,8 +232,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
(applicationContext as ApplicationContext).startPollingIfNeeded()
|
(applicationContext as ApplicationContext).startPollingIfNeeded()
|
||||||
// update things based on TextSecurePrefs (profile info etc)
|
// update things based on TextSecurePrefs (profile info etc)
|
||||||
// Set up remaining components if needed
|
// Set up remaining components if needed
|
||||||
val application = ApplicationContext.getInstance(this@HomeActivity)
|
pushRegistry.refresh(false)
|
||||||
application.registerForFCMIfNeeded(false)
|
|
||||||
if (textSecurePreferences.getLocalNumber() != null) {
|
if (textSecurePreferences.getLocalNumber() != null) {
|
||||||
OpenGroupManager.startPolling()
|
OpenGroupManager.startPolling()
|
||||||
JobQueue.shared.resumePendingJobs()
|
JobQueue.shared.resumePendingJobs()
|
||||||
@ -298,13 +299,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
EventBus.getDefault().register(this@HomeActivity)
|
EventBus.getDefault().register(this@HomeActivity)
|
||||||
if (intent.hasExtra(FROM_ONBOARDING)
|
if (intent.hasExtra(FROM_ONBOARDING)
|
||||||
&& intent.getBooleanExtra(FROM_ONBOARDING, false)
|
&& intent.getBooleanExtra(FROM_ONBOARDING, false)) {
|
||||||
&& !(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()
|
if ((getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) {
|
||||||
) {
|
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
.request(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
|
configFactory.user?.let { user ->
|
||||||
|
if (!user.isBlockCommunityMessageRequestsSet()) {
|
||||||
|
user.setCommunityMessageRequests(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInputFocusChanged(hasFocus: Boolean) {
|
override fun onInputFocusChanged(hasFocus: Boolean) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.linkpreview;
|
package org.thoughtcrime.securesms.linkpreview;
|
||||||
|
|
||||||
|
import static org.session.libsession.utilities.Util.readFully;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
@ -8,8 +10,6 @@ import android.net.Uri;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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.Attachment;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
||||||
@ -148,7 +148,7 @@ public class LinkPreviewRepository {
|
|||||||
InputStream bodyStream = response.body().byteStream();
|
InputStream bodyStream = response.body().byteStream();
|
||||||
controller.setStream(bodyStream);
|
controller.setStream(bodyStream);
|
||||||
|
|
||||||
byte[] data = IOUtils.readInputStreamFully(bodyStream);
|
byte[] data = readFully(bodyStream);
|
||||||
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||||
Optional<Attachment> thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaTypes.IMAGE_JPEG);
|
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.EmojiKeyboardProvider;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
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.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
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.Log;
|
||||||
import org.session.libsignal.utilities.Util;
|
import org.session.libsignal.utilities.Util;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
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.ConversationActivityV2;
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities;
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities;
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
|
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();
|
.get();
|
||||||
setLargeIcon(iconBitmap);
|
setLargeIcon(iconBitmap);
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, "get iconBitmap in getThread failed", e);
|
||||||
setLargeIcon(getPlaceholderDrawable(context, recipient));
|
setLargeIcon(getPlaceholderDrawable(context, recipient));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -298,7 +298,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
.submit(64, 64)
|
.submit(64, 64)
|
||||||
.get();
|
.get();
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, "getBigPicture failed", e);
|
||||||
return Bitmap.createBitmap(64, 64, Bitmap.Config.RGB_565);
|
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 android.widget.Toast
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityPnModeBinding
|
import network.loki.messenger.databinding.ActivityPnModeBinding
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
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.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
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.showSessionDialog
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
import org.thoughtcrime.securesms.util.PNModeView
|
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.getColorWithID
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class PNModeActivity : BaseActionBarActivity() {
|
class PNModeActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
|
@Inject lateinit var pushRegistry: PushRegistry
|
||||||
|
|
||||||
private lateinit var binding: ActivityPnModeBinding
|
private lateinit var binding: ActivityPnModeBinding
|
||||||
private var selectedOptionView: PNModeView? = null
|
private var selectedOptionView: PNModeView? = null
|
||||||
|
|
||||||
@ -158,10 +166,10 @@ class PNModeActivity : BaseActionBarActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
|
TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView))
|
||||||
val application = ApplicationContext.getInstance(this)
|
val application = ApplicationContext.getInstance(this)
|
||||||
application.startPollingIfNeeded()
|
application.startPollingIfNeeded()
|
||||||
application.registerForFCMIfNeeded(true)
|
pushRegistry.refresh(true)
|
||||||
val intent = Intent(this, HomeActivity::class.java)
|
val intent = Intent(this, HomeActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
intent.putExtra(HomeActivity.FROM_ONBOARDING, true)
|
intent.putExtra(HomeActivity.FROM_ONBOARDING, true)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms.preferences
|
package org.thoughtcrime.securesms.preferences
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment
|
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() {
|
class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
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
|
package org.thoughtcrime.securesms.preferences
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class PrivacySettingsActivity : PassphraseRequiredActionBarActivity() {
|
class PrivacySettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||||
|
@ -8,6 +8,9 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.preference.Preference
|
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.BuildConfig
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
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.session.libsession.utilities.TextSecurePreferences.Companion.setScreenLockEnabled
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNotificationsEnabled
|
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNotificationsEnabled
|
||||||
import org.thoughtcrime.securesms.util.IntentUtils
|
import org.thoughtcrime.securesms.util.IntentUtils
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
|
class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
|
||||||
|
|
||||||
|
@Inject lateinit var configFactory: ConfigFactory
|
||||||
|
|
||||||
override fun onCreate(paramBundle: Bundle?) {
|
override fun onCreate(paramBundle: Bundle?) {
|
||||||
super.onCreate(paramBundle)
|
super.onCreate(paramBundle)
|
||||||
findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK)!!
|
findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK)!!
|
||||||
@ -30,6 +39,33 @@ class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
|
|||||||
.onPreferenceChangeListener = TypingIndicatorsToggleListener()
|
.onPreferenceChangeListener = TypingIndicatorsToggleListener()
|
||||||
findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!!
|
findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!!
|
||||||
.onPreferenceChangeListener = CallToggleListener(this) { setCall(it) }
|
.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()
|
initializeVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class ShareLogsDialog : DialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||||
title(R.string.dialog_share_logs_title)
|
title(R.string.dialog_share_logs_title)
|
||||||
text(R.string.dialog_share_logs_explanation)
|
text(R.string.dialog_share_logs_explanation)
|
||||||
button(R.string.share) { shareLogs() }
|
button(R.string.share, dismiss = false) { shareLogs() }
|
||||||
cancelButton { dismiss() }
|
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
|
package org.thoughtcrime.securesms.repository
|
||||||
|
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
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.database.MessageDataProvider
|
||||||
import org.session.libsession.messaging.messages.Destination
|
import org.session.libsession.messaging.messages.Destination
|
||||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
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.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||||
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
|
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
|
||||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||||
@ -37,6 +44,8 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
|
|
||||||
interface ConversationRepository {
|
interface ConversationRepository {
|
||||||
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||||
|
fun maybeGetBlindedRecipient(recipient: Recipient): Recipient?
|
||||||
|
fun recipientUpdateFlow(threadId: Long): Flow<Recipient?>
|
||||||
fun saveDraft(threadId: Long, text: String)
|
fun saveDraft(threadId: Long, text: String)
|
||||||
fun getDraft(threadId: Long): String?
|
fun getDraft(threadId: Long): String?
|
||||||
fun clearDrafts(threadId: Long)
|
fun clearDrafts(threadId: Long)
|
||||||
@ -77,6 +86,7 @@ interface ConversationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DefaultConversationRepository @Inject constructor(
|
class DefaultConversationRepository @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
private val textSecurePreferences: TextSecurePreferences,
|
private val textSecurePreferences: TextSecurePreferences,
|
||||||
private val messageDataProvider: MessageDataProvider,
|
private val messageDataProvider: MessageDataProvider,
|
||||||
private val threadDb: ThreadDatabase,
|
private val threadDb: ThreadDatabase,
|
||||||
@ -90,13 +100,29 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
private val lokiMessageDb: LokiMessageDatabase,
|
private val lokiMessageDb: LokiMessageDatabase,
|
||||||
private val sessionJobDb: SessionJobDatabase,
|
private val sessionJobDb: SessionJobDatabase,
|
||||||
private val configDb: ExpirationConfigurationDatabase,
|
private val configDb: ExpirationConfigurationDatabase,
|
||||||
private val configFactory: ConfigFactory
|
private val configFactory: ConfigFactory,
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
) : ConversationRepository {
|
) : ConversationRepository {
|
||||||
|
|
||||||
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
|
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
|
||||||
return threadDb.getRecipientForThreadId(threadId)
|
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) {
|
override fun saveDraft(threadId: Long, text: String) {
|
||||||
if (text.isEmpty()) return
|
if (text.isEmpty()) return
|
||||||
val drafts = DraftDatabase.Drafts()
|
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.Editable;
|
||||||
import android.text.TextWatcher;
|
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_notifications__priority">Priority</string>
|
||||||
<string name="preferences_app_protection__screenshot_notifications">Screenshot Notifications</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_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 -->
|
<!-- 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_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_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_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>
|
<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>
|
||||||
|
|
||||||
|
<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">
|
<PreferenceCategory android:title="@string/preferences__read_receipts">
|
||||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
android:defaultValue="false"
|
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
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import org.hamcrest.CoreMatchers.endsWith
|
import org.hamcrest.CoreMatchers.endsWith
|
||||||
import org.hamcrest.CoreMatchers.equalTo
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
|
import org.hamcrest.CoreMatchers.notNullValue
|
||||||
|
import org.hamcrest.CoreMatchers.nullValue
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mockito.Mockito.anyLong
|
import org.mockito.Mockito.anyLong
|
||||||
import org.mockito.Mockito.anySet
|
import org.mockito.Mockito.anySet
|
||||||
import org.mockito.Mockito.mock
|
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.BaseViewModelTest
|
import org.thoughtcrime.securesms.BaseViewModelTest
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
import org.thoughtcrime.securesms.repository.ResultOf
|
import org.thoughtcrime.securesms.repository.ResultOf
|
||||||
import org.mockito.Mockito.`when` as whenever
|
|
||||||
|
|
||||||
class ConversationViewModelTest: BaseViewModelTest() {
|
class ConversationViewModelTest: BaseViewModelTest() {
|
||||||
|
|
||||||
private val repository = mock(ConversationRepository::class.java)
|
private val repository = mock<ConversationRepository>()
|
||||||
private val storage = mock(Storage::class.java)
|
private val storage = mock<Storage>()
|
||||||
|
|
||||||
private val threadId = 123L
|
private val threadId = 123L
|
||||||
private val edKeyPair = mock(KeyPair::class.java)
|
private val edKeyPair = mock<KeyPair>()
|
||||||
private lateinit var recipient: Recipient
|
private lateinit var recipient: Recipient
|
||||||
|
|
||||||
private val viewModel: ConversationViewModel by lazy {
|
private val viewModel: ConversationViewModel by lazy {
|
||||||
ConversationViewModel(threadId, edKeyPair, mock(), repository, storage)
|
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
recipient = mock(Recipient::class.java)
|
recipient = mock()
|
||||||
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
|
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
|
||||||
|
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -79,7 +83,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should delete locally`() {
|
fun `should delete locally`() {
|
||||||
val message = mock(MessageRecord::class.java)
|
val message = mock<MessageRecord>()
|
||||||
|
|
||||||
viewModel.deleteLocally(message)
|
viewModel.deleteLocally(message)
|
||||||
|
|
||||||
@ -88,7 +92,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest {
|
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()
|
val error = Throwable()
|
||||||
whenever(repository.deleteForEveryone(anyLong(), any(), any()))
|
whenever(repository.deleteForEveryone(anyLong(), any(), any()))
|
||||||
.thenReturn(ResultOf.Failure(error))
|
.thenReturn(ResultOf.Failure(error))
|
||||||
@ -101,7 +105,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `should emit error message on failure to delete messages without unsend request`() =
|
fun `should emit error message on failure to delete messages without unsend request`() =
|
||||||
runBlockingTest {
|
runBlockingTest {
|
||||||
val message = mock(MessageRecord::class.java)
|
val message = mock<MessageRecord>()
|
||||||
val error = Throwable()
|
val error = Throwable()
|
||||||
whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet()))
|
whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet()))
|
||||||
.thenReturn(ResultOf.Failure(error))
|
.thenReturn(ResultOf.Failure(error))
|
||||||
@ -181,4 +185,30 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(0))
|
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 {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
if (project.hasProperty('huawei')) maven {
|
||||||
|
url 'https://developer.huawei.com/repo/'
|
||||||
|
content {
|
||||||
|
includeGroup 'com.huawei.agconnect'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
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-gradle-plugin:$kotlinVersion"
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||||
classpath "com.google.gms:google-services:$googleServicesVersion"
|
classpath "com.google.gms:google-services:$googleServicesVersion"
|
||||||
classpath files('libs/gradle-witness.jar')
|
classpath files('libs/gradle-witness.jar')
|
||||||
classpath "com.squareup:javapoet:1.13.0"
|
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()
|
jcenter()
|
||||||
maven { url "https://jitpack.io" }
|
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 {
|
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));
|
auto expiry = util::serialize_expiry(env, session::config::expiration_mode::after_send, std::chrono::seconds(*nts_expiry));
|
||||||
return 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 getNtsPriority(): Int
|
||||||
external fun setNtsExpiry(expiryMode: ExpiryMode)
|
external fun setNtsExpiry(expiryMode: ExpiryMode)
|
||||||
external fun getNtsExpiry(): ExpiryMode
|
external fun getNtsExpiry(): ExpiryMode
|
||||||
|
external fun getCommunityMessageRequests(): Boolean
|
||||||
|
external fun setCommunityMessageRequests(blocks: Boolean)
|
||||||
|
external fun isBlockCommunityMessageRequestsSet(): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) {
|
class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.library'
|
id 'com.android.library'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
|
id 'kotlinx-serialization'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -41,6 +42,7 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||||
|
@ -43,6 +43,7 @@ interface StorageProtocol {
|
|||||||
fun getUserProfile(): Profile
|
fun getUserProfile(): Profile
|
||||||
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
|
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
|
||||||
fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
|
fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
|
||||||
|
fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean)
|
||||||
fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
|
fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
|
||||||
fun clearUserPic()
|
fun clearUserPic()
|
||||||
// Signal
|
// Signal
|
||||||
@ -232,4 +233,5 @@ interface StorageProtocol {
|
|||||||
fun notifyConfigUpdates(forConfigObject: ConfigBase, messageTimestamp: Long)
|
fun notifyConfigUpdates(forConfigObject: ConfigBase, messageTimestamp: Long)
|
||||||
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
|
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
|
||||||
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): 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.MessageDataProvider
|
||||||
import org.session.libsession.database.StorageProtocol
|
import org.session.libsession.database.StorageProtocol
|
||||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||||
|
import org.session.libsession.utilities.Device
|
||||||
|
|
||||||
class MessagingModuleConfiguration(
|
class MessagingModuleConfiguration(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val storage: StorageProtocol,
|
val storage: StorageProtocol,
|
||||||
|
val device: Device,
|
||||||
val messageDataProvider: MessageDataProvider,
|
val messageDataProvider: MessageDataProvider,
|
||||||
val getUserED25519KeyPair: () -> KeyPair?,
|
val getUserED25519KeyPair: () -> KeyPair?,
|
||||||
val configFactory: ConfigFactoryProtocol
|
val configFactory: ConfigFactoryProtocol
|
||||||
|
@ -3,12 +3,11 @@ package org.session.libsession.messaging.jobs
|
|||||||
import com.esotericsoftware.kryo.Kryo
|
import com.esotericsoftware.kryo.Kryo
|
||||||
import com.esotericsoftware.kryo.io.Input
|
import com.esotericsoftware.kryo.io.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
|
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.messaging.utilities.Data
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.snode.SnodeMessage
|
import org.session.libsession.snode.SnodeMessage
|
||||||
@ -31,23 +30,27 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute(dispatcherName: String) {
|
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 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 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) {
|
retryIfNeeded(4) {
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response ->
|
OnionRequestAPI.sendOnionRequest(
|
||||||
val code = response.info["code"] as? Int
|
request,
|
||||||
if (code == null || code == 0) {
|
server.url,
|
||||||
Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.")
|
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 ->
|
} fail { exception ->
|
||||||
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: $exception.")
|
||||||
}
|
}
|
||||||
}.success {
|
} success {
|
||||||
handleSuccess(dispatcherName)
|
handleSuccess(dispatcherName)
|
||||||
}. fail {
|
} fail {
|
||||||
handleFailure(dispatcherName, it)
|
handleFailure(dispatcherName, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,8 @@ class VisibleMessage(
|
|||||||
var profile: Profile? = null,
|
var profile: Profile? = null,
|
||||||
var openGroupInvitation: OpenGroupInvitation? = null,
|
var openGroupInvitation: OpenGroupInvitation? = null,
|
||||||
var reaction: Reaction? = null,
|
var reaction: Reaction? = null,
|
||||||
var hasMention: Boolean = false
|
var hasMention: Boolean = false,
|
||||||
|
var blocksMessageRequests: Boolean = false
|
||||||
) : Message() {
|
) : Message() {
|
||||||
|
|
||||||
override val isSelfSendValid: Boolean = true
|
override val isSelfSendValid: Boolean = true
|
||||||
@ -71,6 +72,9 @@ class VisibleMessage(
|
|||||||
val reaction = Reaction.fromProto(reactionProto)
|
val reaction = Reaction.fromProto(reactionProto)
|
||||||
result.reaction = reaction
|
result.reaction = reaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.blocksMessageRequests = with (dataMessage) { hasBlocksCommunityMessageRequests() && blocksCommunityMessageRequests }
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,6 +134,8 @@ class VisibleMessage(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Community blocked message requests flag
|
||||||
|
dataMessage.blocksCommunityMessageRequests = blocksMessageRequests
|
||||||
// Sync target
|
// Sync target
|
||||||
if (syncTarget != null) {
|
if (syncTarget != null) {
|
||||||
dataMessage.syncTarget = syncTarget
|
dataMessage.syncTarget = syncTarget
|
||||||
|
@ -753,7 +753,8 @@ object OpenGroupApi {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val serverCapabilities = storage.getServerCapabilities(server)
|
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(
|
requests.add(
|
||||||
if (lastInboxMessageId == null) {
|
if (lastInboxMessageId == null) {
|
||||||
BatchRequestInfo(
|
BatchRequestInfo(
|
||||||
|
@ -30,6 +30,7 @@ import org.session.libsession.snode.SnodeAPI
|
|||||||
import org.session.libsession.snode.SnodeMessage
|
import org.session.libsession.snode.SnodeMessage
|
||||||
import org.session.libsession.snode.SnodeModule
|
import org.session.libsession.snode.SnodeModule
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.Device
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.SSKEnvironment
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
import org.session.libsignal.crypto.PushTransportDetails
|
import org.session.libsignal.crypto.PushTransportDetails
|
||||||
@ -266,9 +267,16 @@ object MessageSender {
|
|||||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||||
if (message.sentTimestamp == null) {
|
if (message.sentTimestamp == null) {
|
||||||
message.sentTimestamp = SnodeAPI.nowWithOffset
|
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()!!
|
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
||||||
var serverCapabilities = listOf<String>()
|
var serverCapabilities = listOf<String>()
|
||||||
var blindedPublicKey: ByteArray? = null
|
var blindedPublicKey: ByteArray? = null
|
||||||
@ -479,8 +487,8 @@ object MessageSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Closed groups
|
// Closed groups
|
||||||
fun createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
fun createClosedGroup(device: Device, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||||
return create(name, members)
|
return create(device, name, members)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun explicitNameChange(groupPublicKey: String, newName: String) {
|
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.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
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.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
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.GroupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.crypto.ecc.Curve
|
import org.session.libsignal.crypto.ecc.Curve
|
||||||
@ -32,7 +33,11 @@ const val groupSizeLimit = 100
|
|||||||
|
|
||||||
val pendingKeyPairs = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
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>()
|
val deferred = deferred<String, Exception>()
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
// Prepare
|
// 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
|
// Add the group to the config now that it was successfully created
|
||||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair)
|
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair)
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
PushRegistryV1.register(device = device, publicKey = userPublicKey)
|
||||||
// Start polling
|
// Start polling
|
||||||
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||||
// Fulfill the promise
|
// 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.attachments.PointerAttachment
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
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.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.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
@ -397,6 +397,10 @@ fun MessageReceiver.handleVisibleMessage(
|
|||||||
profileManager.setProfilePicture(context, recipient, null, null)
|
profileManager.setProfilePicture(context, recipient, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userPublicKey != messageSender && !isUserBlindedSender) {
|
||||||
|
storage.setBlocksCommunityMessageRequests(recipient, message.blocksMessageRequests)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Parse quote if needed
|
// Parse quote if needed
|
||||||
var quoteModel: QuoteModel? = null
|
var quoteModel: QuoteModel? = null
|
||||||
@ -649,10 +653,14 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
|||||||
// Set expiration timer
|
// Set expiration timer
|
||||||
storage.setExpirationTimer(groupID, expireTimer)
|
storage.setExpirationTimer(groupID, expireTimer)
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
PushRegistryV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey)
|
||||||
// Create thread
|
// Notify the user
|
||||||
val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
if (userPublicKey == sender && !groupExists) {
|
||||||
storage.setThreadDate(threadId, formationTimestamp)
|
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
|
// Start polling
|
||||||
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||||
}
|
}
|
||||||
@ -958,7 +966,7 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou
|
|||||||
storage.setActive(groupID, false)
|
storage.setActive(groupID, false)
|
||||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
PushRegistryV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey)
|
||||||
// Stop polling
|
// Stop polling
|
||||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
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? {
|
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)
|
val plaintext = ByteArray(plaintextSize)
|
||||||
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||||
plaintext,
|
plaintext,
|
||||||
|
@ -686,4 +686,7 @@ enum class Version(val value: String) {
|
|||||||
data class OnionResponse(
|
data class OnionResponse(
|
||||||
val info: Map<*, *>,
|
val info: Map<*, *>,
|
||||||
val body: ByteArray? = null
|
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
|
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.messages.SignalServiceGroup
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -15,8 +17,15 @@ object GroupUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): String {
|
fun getEncodedOpenGroupInboxID(openGroup: OpenGroup, sessionId: SessionId): Address {
|
||||||
return OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID)
|
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
|
@JvmStatic
|
||||||
@ -51,7 +60,7 @@ object GroupUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getDecodedOpenGroupInbox(groupID: String): String {
|
fun getDecodedOpenGroupInboxSessionId(groupID: String): String {
|
||||||
val decodedGroupId = getDecodedGroupID(groupID)
|
val decodedGroupId = getDecodedGroupID(groupID)
|
||||||
if (decodedGroupId.split("!").count() > 2) {
|
if (decodedGroupId.split("!").count() > 2) {
|
||||||
return decodedGroupId.split("!", limit = 3)[2]
|
return decodedGroupId.split("!", limit = 3)[2]
|
||||||
|
@ -37,12 +37,12 @@ interface TextSecurePreferences {
|
|||||||
fun setLastConfigurationSyncTime(value: Long)
|
fun setLastConfigurationSyncTime(value: Long)
|
||||||
fun getConfigurationMessageSynced(): Boolean
|
fun getConfigurationMessageSynced(): Boolean
|
||||||
fun setConfigurationMessageSynced(value: Boolean)
|
fun setConfigurationMessageSynced(value: Boolean)
|
||||||
fun isUsingFCM(): Boolean
|
fun isPushEnabled(): Boolean
|
||||||
fun setIsUsingFCM(value: Boolean)
|
fun setPushEnabled(value: Boolean)
|
||||||
fun getFCMToken(): String?
|
fun getPushToken(): String?
|
||||||
fun setFCMToken(value: String)
|
fun setPushToken(value: String)
|
||||||
fun getLastFCMUploadTime(): Long
|
fun getPushRegisterTime(): Long
|
||||||
fun setLastFCMUploadTime(value: Long)
|
fun setPushRegisterTime(value: Long)
|
||||||
fun isScreenLockEnabled(): Boolean
|
fun isScreenLockEnabled(): Boolean
|
||||||
fun setScreenLockEnabled(value: Boolean)
|
fun setScreenLockEnabled(value: Boolean)
|
||||||
fun getScreenLockTimeout(): Long
|
fun getScreenLockTimeout(): Long
|
||||||
@ -251,9 +251,9 @@ interface TextSecurePreferences {
|
|||||||
const val LINK_PREVIEWS = "pref_link_previews"
|
const val LINK_PREVIEWS = "pref_link_previews"
|
||||||
const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning"
|
const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning"
|
||||||
const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
|
const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
|
||||||
const val IS_USING_FCM = "pref_is_using_fcm"
|
const val IS_PUSH_ENABLED = "pref_is_using_fcm"
|
||||||
const val FCM_TOKEN = "pref_fcm_token"
|
const val PUSH_TOKEN = "pref_fcm_token_2"
|
||||||
const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_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 LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
|
||||||
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
||||||
const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
|
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_DARK = "ocean.dark"
|
||||||
const val OCEAN_LIGHT = "ocean.light"
|
const val OCEAN_LIGHT = "ocean.light"
|
||||||
|
|
||||||
|
const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getLastConfigurationSyncTime(context: Context): Long {
|
fun getLastConfigurationSyncTime(context: Context): Long {
|
||||||
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
|
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
|
||||||
@ -309,31 +311,31 @@ interface TextSecurePreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isUsingFCM(context: Context): Boolean {
|
fun isPushEnabled(context: Context): Boolean {
|
||||||
return getBooleanPreference(context, IS_USING_FCM, false)
|
return getBooleanPreference(context, IS_PUSH_ENABLED, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setIsUsingFCM(context: Context, value: Boolean) {
|
fun setPushEnabled(context: Context, value: Boolean) {
|
||||||
setBooleanPreference(context, IS_USING_FCM, value)
|
setBooleanPreference(context, IS_PUSH_ENABLED, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getFCMToken(context: Context): String? {
|
fun getPushToken(context: Context): String? {
|
||||||
return getStringPreference(context, FCM_TOKEN, "")
|
return getStringPreference(context, PUSH_TOKEN, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setFCMToken(context: Context, value: String) {
|
fun setPushToken(context: Context, value: String?) {
|
||||||
setStringPreference(context, FCM_TOKEN, value)
|
setStringPreference(context, PUSH_TOKEN, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLastFCMUploadTime(context: Context): Long {
|
fun getPushRegisterTime(context: Context): Long {
|
||||||
return getLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, 0)
|
return getLongPreference(context, PUSH_REGISTER_TIME, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLastFCMUploadTime(context: Context, value: Long) {
|
fun setPushRegisterTime(context: Context, value: Long) {
|
||||||
setLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, value)
|
setLongPreference(context, PUSH_REGISTER_TIME, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
@ -1008,7 +1010,6 @@ interface TextSecurePreferences {
|
|||||||
fun clearAll(context: Context) {
|
fun clearAll(context: Context) {
|
||||||
getDefaultSharedPreferences(context).edit().clear().commit()
|
getDefaultSharedPreferences(context).edit().clear().commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1033,28 +1034,28 @@ class AppTextSecurePreferences @Inject constructor(
|
|||||||
TextSecurePreferences._events.tryEmit(TextSecurePreferences.CONFIGURATION_SYNCED)
|
TextSecurePreferences._events.tryEmit(TextSecurePreferences.CONFIGURATION_SYNCED)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isUsingFCM(): Boolean {
|
override fun isPushEnabled(): Boolean {
|
||||||
return getBooleanPreference(TextSecurePreferences.IS_USING_FCM, false)
|
return getBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setIsUsingFCM(value: Boolean) {
|
override fun setPushEnabled(value: Boolean) {
|
||||||
setBooleanPreference(TextSecurePreferences.IS_USING_FCM, value)
|
setBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFCMToken(): String? {
|
override fun getPushToken(): String? {
|
||||||
return getStringPreference(TextSecurePreferences.FCM_TOKEN, "")
|
return getStringPreference(TextSecurePreferences.PUSH_TOKEN, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setFCMToken(value: String) {
|
override fun setPushToken(value: String) {
|
||||||
setStringPreference(TextSecurePreferences.FCM_TOKEN, value)
|
setStringPreference(TextSecurePreferences.PUSH_TOKEN, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastFCMUploadTime(): Long {
|
override fun getPushRegisterTime(): Long {
|
||||||
return getLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, 0)
|
return getLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setLastFCMUploadTime(value: Long) {
|
override fun setPushRegisterTime(value: Long) {
|
||||||
setLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, value)
|
setLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isScreenLockEnabled(): Boolean {
|
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 String notificationChannel;
|
||||||
private boolean forceSmsSelection;
|
private boolean forceSmsSelection;
|
||||||
private String wrapperHash;
|
private String wrapperHash;
|
||||||
|
private boolean blocksCommunityMessageRequests;
|
||||||
|
|
||||||
private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED;
|
private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED;
|
||||||
|
|
||||||
@ -194,6 +195,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
this.unidentifiedAccessMode = details.get().unidentifiedAccessMode;
|
this.unidentifiedAccessMode = details.get().unidentifiedAccessMode;
|
||||||
this.forceSmsSelection = details.get().forceSmsSelection;
|
this.forceSmsSelection = details.get().forceSmsSelection;
|
||||||
this.notifyType = details.get().notifyType;
|
this.notifyType = details.get().notifyType;
|
||||||
|
this.blocksCommunityMessageRequests = details.get().blocksCommunityMessageRequests;
|
||||||
this.disappearingState = details.get().disappearingState;
|
this.disappearingState = details.get().disappearingState;
|
||||||
|
|
||||||
this.participants.clear();
|
this.participants.clear();
|
||||||
@ -232,6 +234,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
Recipient.this.forceSmsSelection = result.forceSmsSelection;
|
Recipient.this.forceSmsSelection = result.forceSmsSelection;
|
||||||
Recipient.this.notifyType = result.notifyType;
|
Recipient.this.notifyType = result.notifyType;
|
||||||
Recipient.this.disappearingState = result.disappearingState;
|
Recipient.this.disappearingState = result.disappearingState;
|
||||||
|
Recipient.this.blocksCommunityMessageRequests = result.blocksCommunityMessageRequests;
|
||||||
|
|
||||||
Recipient.this.participants.clear();
|
Recipient.this.participants.clear();
|
||||||
Recipient.this.participants.addAll(result.participants);
|
Recipient.this.participants.addAll(result.participants);
|
||||||
@ -285,6 +288,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
|
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
|
||||||
this.forceSmsSelection = details.forceSmsSelection;
|
this.forceSmsSelection = details.forceSmsSelection;
|
||||||
this.wrapperHash = details.wrapperHash;
|
this.wrapperHash = details.wrapperHash;
|
||||||
|
this.blocksCommunityMessageRequests = details.blocksCommunityMessageRequests;
|
||||||
|
|
||||||
this.participants.addAll(details.participants);
|
this.participants.addAll(details.participants);
|
||||||
this.resolving = false;
|
this.resolving = false;
|
||||||
@ -325,7 +329,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
} else if (isOpenGroupInboxRecipient()){
|
} else if (isOpenGroupInboxRecipient()){
|
||||||
String inboxID = GroupUtil.getDecodedOpenGroupInbox(sessionID);
|
String inboxID = GroupUtil.getDecodedOpenGroupInboxSessionId(sessionID);
|
||||||
Contact contact = storage.getContactWithSessionID(inboxID);
|
Contact contact = storage.getContactWithSessionID(inboxID);
|
||||||
if (contact == null) { return sessionID; }
|
if (contact == null) { return sessionID; }
|
||||||
return contact.displayName(Contact.ContactContext.REGULAR);
|
return contact.displayName(Contact.ContactContext.REGULAR);
|
||||||
@ -349,6 +353,18 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
if (notify) notifyListeners();
|
if (notify) notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getBlocksCommunityMessageRequests() {
|
||||||
|
return blocksCommunityMessageRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlocksCommunityMessageRequests(boolean blocksCommunityMessageRequests) {
|
||||||
|
synchronized (this) {
|
||||||
|
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized @NonNull MaterialColor getColor() {
|
public synchronized @NonNull MaterialColor getColor() {
|
||||||
if (isGroupRecipient()) return MaterialColor.GROUP;
|
if (isGroupRecipient()) return MaterialColor.GROUP;
|
||||||
else if (color != null) return color;
|
else if (color != null) return color;
|
||||||
@ -779,12 +795,43 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
Recipient recipient = (Recipient) o;
|
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
|
@Override
|
||||||
public int hashCode() {
|
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);
|
result = 31 * result + Arrays.hashCode(profileKey);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -908,6 +955,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||||
private final boolean forceSmsSelection;
|
private final boolean forceSmsSelection;
|
||||||
private final String wrapperHash;
|
private final String wrapperHash;
|
||||||
|
private final boolean blocksCommunityMessageRequests;
|
||||||
|
|
||||||
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
|
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
|
||||||
int notifyType,
|
int notifyType,
|
||||||
@ -931,7 +979,9 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
@Nullable String notificationChannel,
|
@Nullable String notificationChannel,
|
||||||
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
|
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
|
||||||
boolean forceSmsSelection,
|
boolean forceSmsSelection,
|
||||||
String wrapperHash)
|
String wrapperHash,
|
||||||
|
boolean blocksCommunityMessageRequests
|
||||||
|
)
|
||||||
{
|
{
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
this.approved = approved;
|
this.approved = approved;
|
||||||
@ -959,6 +1009,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||||
this.forceSmsSelection = forceSmsSelection;
|
this.forceSmsSelection = forceSmsSelection;
|
||||||
this.wrapperHash = wrapperHash;
|
this.wrapperHash = wrapperHash;
|
||||||
|
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable MaterialColor getColor() {
|
public @Nullable MaterialColor getColor() {
|
||||||
@ -1065,6 +1116,10 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
return wrapperHash;
|
return wrapperHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getBlocksCommunityMessageRequests() {
|
||||||
|
return blocksCommunityMessageRequests;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -180,6 +180,7 @@ class RecipientProvider {
|
|||||||
@NonNull final UnidentifiedAccessMode unidentifiedAccessMode;
|
@NonNull final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||||
final boolean forceSmsSelection;
|
final boolean forceSmsSelection;
|
||||||
final String wrapperHash;
|
final String wrapperHash;
|
||||||
|
final boolean blocksCommunityMessageRequests;
|
||||||
|
|
||||||
RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId,
|
RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId,
|
||||||
boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings,
|
boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings,
|
||||||
@ -214,6 +215,7 @@ class RecipientProvider {
|
|||||||
this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED;
|
this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED;
|
||||||
this.forceSmsSelection = settings != null && settings.isForceSmsSelection();
|
this.forceSmsSelection = settings != null && settings.isForceSmsSelection();
|
||||||
this.wrapperHash = settings != null ? settings.getWrapperHash() : null;
|
this.wrapperHash = settings != null ? settings.getWrapperHash() : null;
|
||||||
|
this.blocksCommunityMessageRequests = settings != null && settings.getBlocksCommunityMessageRequests();
|
||||||
|
|
||||||
if (name == null && settings != null) this.name = settings.getSystemDisplayName();
|
if (name == null && settings != null) this.name = settings.getSystemDisplayName();
|
||||||
else this.name = name;
|
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