mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-22 16:07:30 +00:00
[SES-469] Update push notifications
- Utilise new push endpoint - Add huawei support
This commit is contained in:
commit
f6345c86ce
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.5.0-alpha02'
|
implementation 'androidx.compose.material:material:1.5.0-alpha02'
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
@ -310,14 +310,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();
|
||||||
|
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
|
||||||
|
}
|
@ -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";
|
||||||
|
|
||||||
|
@ -50,7 +50,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
|
||||||
@ -591,7 +591,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)
|
||||||
|
@ -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()
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
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!! }
|
||||||
|
}
|
||||||
|
}
|
@ -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,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'
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,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
|
||||||
@ -454,8 +455,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.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
@ -33,7 +34,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
|
||||||
@ -89,7 +94,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
|
||||||
|
@ -23,7 +23,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
|
||||||
@ -556,10 +556,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)
|
||||||
}
|
}
|
||||||
@ -865,7 +869,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");
|
||||||
|
}
|
@ -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"
|
||||||
@ -309,31 +309,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 +1008,6 @@ interface TextSecurePreferences {
|
|||||||
fun clearAll(context: Context) {
|
fun clearAll(context: Context) {
|
||||||
getDefaultSharedPreferences(context).edit().clear().commit()
|
getDefaultSharedPreferences(context).edit().clear().commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1033,28 +1032,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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,7 +15,7 @@ object ExternalStorageUtil {
|
|||||||
@Throws(NoExternalStorageException::class)
|
@Throws(NoExternalStorageException::class)
|
||||||
fun getDir(context: Context, type: String?): File {
|
fun getDir(context: Context, type: String?): File {
|
||||||
return context.getExternalFilesDir(type)
|
return context.getExternalFilesDir(type)
|
||||||
?: throw NoExternalStorageException("External storage dir is currently unavailable: $type")
|
?: throw NoExternalStorageException("External storage dir is currently unavailable: $type")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(NoExternalStorageException::class)
|
@Throws(NoExternalStorageException::class)
|
||||||
@ -73,10 +73,7 @@ object ExternalStorageUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getCleanFileName(fileName: String?): String? {
|
fun getCleanFileName(fileName: String?): String? =
|
||||||
var fileName = fileName ?: return null
|
fileName?.replace('\u202D', '\uFFFD')
|
||||||
fileName = fileName.replace('\u202D', '\uFFFD')
|
?.replace('\u202E', '\uFFFD')
|
||||||
fileName = fileName.replace('\u202E', '\uFFFD')
|
|
||||||
return fileName
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -3,8 +3,13 @@ package org.session.libsignal.utilities
|
|||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
|
import nl.komponents.kovenant.functional.map
|
||||||
|
import nl.komponents.kovenant.task
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
fun emptyPromise() = EMPTY_PROMISE
|
||||||
|
private val EMPTY_PROMISE: Promise<*, java.lang.Exception> = task {}
|
||||||
|
|
||||||
fun <V, E : Throwable> Promise<V, E>.get(defaultValue: V): V {
|
fun <V, E : Throwable> Promise<V, E>.get(defaultValue: V): V {
|
||||||
return try {
|
return try {
|
||||||
get()
|
get()
|
||||||
@ -55,3 +60,10 @@ fun <V> Promise<V, Exception>.timeout(millis: Long): Promise<V, Exception> {
|
|||||||
}
|
}
|
||||||
return deferred.promise
|
return deferred.promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
infix fun <V, E: Exception> Promise<V, E>.sideEffect(
|
||||||
|
callback: (value: V) -> Unit
|
||||||
|
) = map {
|
||||||
|
callback(it)
|
||||||
|
it
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user