mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-22 07:57: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
|
||||
*.sh
|
||||
pkcs11.password
|
||||
play
|
||||
app/play
|
||||
|
@ -34,6 +34,12 @@ Setting up a development environment and building from Android Studio
|
||||
6. Project initialization and building should proceed.
|
||||
7. Clone submodules with `git submodule update --init --recursive`
|
||||
|
||||
If you would like to build the Huawei Flavor with Huawei HMS push notifications you will need to pass 'huawei' as a command line arg to include the required dependencies.
|
||||
|
||||
e.g. `./gradlew assembleHuaweiDebug -Phuawei`
|
||||
|
||||
If you are building in Android Studio then add `-Phuawei` to `Preferences > Build, Execution, Deployment > Gradle-Android Compiler > Command-line Options`
|
||||
|
||||
Contributing code
|
||||
-----------------
|
||||
|
||||
|
311
app/build.gradle
311
app/build.gradle
@ -24,15 +24,181 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
|
||||
|
||||
configurations.all {
|
||||
exclude module: "commons-logging"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 354
|
||||
def canonicalVersionName = "1.17.0"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4,
|
||||
'universal' : 5]
|
||||
|
||||
android {
|
||||
compileSdkVersion androidCompileSdkVersion
|
||||
namespace 'network.loki.messenger'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'LICENSE'
|
||||
exclude 'NOTICE'
|
||||
exclude 'asm-license.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.4.7'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion androidMinimumSdkVersion
|
||||
targetSdkVersion androidTargetSdkVersion
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
project.ext.set("archivesBaseName", "session")
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
|
||||
resConfigs autoResConfig()
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
String sharedTestDir = 'src/sharedTest/java'
|
||||
test.java.srcDirs += sharedTestDir
|
||||
androidTest.java.srcDirs += sharedTestDir
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "distribution"
|
||||
productFlavors {
|
||||
play {
|
||||
dimension "distribution"
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
huawei {
|
||||
dimension "distribution"
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.HUAWEI"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
|
||||
}
|
||||
|
||||
website {
|
||||
dimension "distribution"
|
||||
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
def huaweiEnabled = project.properties['huawei'] != null
|
||||
|
||||
applicationVariants.configureEach { variant ->
|
||||
if (variant.flavorName == 'huawei') {
|
||||
variant.getPreBuildProvider().configure { task ->
|
||||
task.doFirst {
|
||||
if (!huaweiEnabled) {
|
||||
def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See BUILDING.md'
|
||||
logger.error(message)
|
||||
throw new GradleException(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.46.1")
|
||||
@ -59,11 +225,12 @@ dependencies {
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
implementation ("com.google.firebase:firebase-messaging:18.0.0") {
|
||||
playImplementation ("com.google.firebase:firebase-messaging:18.0.0") {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
@ -176,144 +343,6 @@ dependencies {
|
||||
implementation 'androidx.compose.material:material:1.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() {
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
return os.toString() + "000"
|
||||
|
26
app/src/huawei/AndroidManifest.xml
Normal file
26
app/src/huawei/AndroidManifest.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application tools:node="merge">
|
||||
<meta-data
|
||||
android:name="com.huawei.hms.client.appid"
|
||||
android:value="appid=107205081">
|
||||
</meta-data>
|
||||
|
||||
<meta-data
|
||||
android:name="com.huawei.hms.client.cpid"
|
||||
android:value="cpid=30061000024605000">
|
||||
</meta-data>
|
||||
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.notifications.HuaweiPushService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
96
app/src/huawei/agconnect-services.json
Normal file
96
app/src/huawei/agconnect-services.json
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"agcgw":{
|
||||
"backurl":"connect-dre.hispace.hicloud.com",
|
||||
"url":"connect-dre.dbankcloud.cn",
|
||||
"websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com",
|
||||
"websocketurl":"connect-ws-dre.hispace.dbankcloud.cn"
|
||||
},
|
||||
"agcgw_all":{
|
||||
"CN":"connect-drcn.dbankcloud.cn",
|
||||
"CN_back":"connect-drcn.hispace.hicloud.com",
|
||||
"DE":"connect-dre.dbankcloud.cn",
|
||||
"DE_back":"connect-dre.hispace.hicloud.com",
|
||||
"RU":"connect-drru.hispace.dbankcloud.ru",
|
||||
"RU_back":"connect-drru.hispace.dbankcloud.cn",
|
||||
"SG":"connect-dra.dbankcloud.cn",
|
||||
"SG_back":"connect-dra.hispace.hicloud.com"
|
||||
},
|
||||
"websocketgw_all":{
|
||||
"CN":"connect-ws-drcn.hispace.dbankcloud.cn",
|
||||
"CN_back":"connect-ws-drcn.hispace.dbankcloud.com",
|
||||
"DE":"connect-ws-dre.hispace.dbankcloud.cn",
|
||||
"DE_back":"connect-ws-dre.hispace.dbankcloud.com",
|
||||
"RU":"connect-ws-drru.hispace.dbankcloud.ru",
|
||||
"RU_back":"connect-ws-drru.hispace.dbankcloud.cn",
|
||||
"SG":"connect-ws-dra.hispace.dbankcloud.cn",
|
||||
"SG_back":"connect-ws-dra.hispace.dbankcloud.com"
|
||||
},
|
||||
"client":{
|
||||
"cp_id":"890061000023000573",
|
||||
"product_id":"99536292102532562",
|
||||
"client_id":"954244311350791232",
|
||||
"client_secret":"555999202D718B6744DAD2E923B386DC17F3F4E29F5105CE0D061EED328DADEE",
|
||||
"project_id":"99536292102532562",
|
||||
"app_id":"107205081",
|
||||
"api_key":"DAEDABeddLEqUy0QRwa1THLwRA0OqrSuyci/HjNvVSmsdWsXRM2U2hRaCyqfvGYH1IFOKrauArssz/WPMLRHCYxliWf+DTj9bDwlWA==",
|
||||
"package_name":"network.loki.messenger"
|
||||
},
|
||||
"oauth_client":{
|
||||
"client_id":"107205081",
|
||||
"client_type":1
|
||||
},
|
||||
"app_info":{
|
||||
"app_id":"107205081",
|
||||
"package_name":"network.loki.messenger"
|
||||
},
|
||||
"service":{
|
||||
"analytics":{
|
||||
"collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||
"collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
||||
"collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
||||
"resource_id":"p1",
|
||||
"channel_id":""
|
||||
},
|
||||
"edukit":{
|
||||
"edu_url":"edukit.edu.cloud.huawei.com.cn",
|
||||
"dh_url":"edukit.edu.cloud.huawei.com.cn"
|
||||
},
|
||||
"search":{
|
||||
"url":"https://search-dre.cloud.huawei.com"
|
||||
},
|
||||
"cloudstorage":{
|
||||
"storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia",
|
||||
"storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu",
|
||||
"storage_url_de":"https://ops-dre.agcstorage.link",
|
||||
"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||
"storage_url_sg":"https://ops-dra.agcstorage.link",
|
||||
"storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn",
|
||||
"storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn"
|
||||
},
|
||||
"ml":{
|
||||
"mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
|
||||
}
|
||||
},
|
||||
"region":"DE",
|
||||
"configuration_version":"3.0",
|
||||
"appInfos":[
|
||||
{
|
||||
"package_name":"network.loki.messenger",
|
||||
"client":{
|
||||
"app_id":"107205081"
|
||||
},
|
||||
"app_info":{
|
||||
"package_name":"network.loki.messenger",
|
||||
"app_id":"107205081"
|
||||
},
|
||||
"oauth_client":{
|
||||
"client_type":1,
|
||||
"client_id":"107205081"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class HuaweiBindingModule {
|
||||
@Binds
|
||||
abstract fun bindTokenFetcher(tokenFetcher: HuaweiTokenFetcher): TokenFetcher
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.os.Bundle
|
||||
import com.huawei.hms.push.HmsMessageService
|
||||
import com.huawei.hms.push.RemoteMessage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.json.JSONException
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.lang.Exception
|
||||
import javax.inject.Inject
|
||||
|
||||
private val TAG = HuaweiPushService::class.java.simpleName
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HuaweiPushService: HmsMessageService() {
|
||||
@Inject lateinit var pushRegistry: PushRegistry
|
||||
@Inject lateinit var pushReceiver: PushReceiver
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage?) {
|
||||
Log.d(TAG, "onMessageReceived")
|
||||
message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPush) ?:
|
||||
pushReceiver.onPush(message?.data?.let(Base64::decode))
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String?) {
|
||||
pushRegistry.register(token)
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String?, bundle: Bundle?) {
|
||||
Log.d(TAG, "New HCM token: $token.")
|
||||
pushRegistry.register(token)
|
||||
}
|
||||
|
||||
override fun onDeletedMessages() {
|
||||
Log.d(TAG, "onDeletedMessages")
|
||||
pushRegistry.refresh(false)
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.huawei.hms.aaid.HmsInstanceId
|
||||
import dagger.Lazy
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.session.libsignal.utilities.Log
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val APP_ID = "107205081"
|
||||
private const val TOKEN_SCOPE = "HCM"
|
||||
|
||||
@Singleton
|
||||
class HuaweiTokenFetcher @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val pushRegistry: Lazy<PushRegistry>,
|
||||
): TokenFetcher {
|
||||
override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run {
|
||||
// https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/push-basic-capability#h2-1576218800370
|
||||
// getToken may return an empty string, if so HuaweiPushService#onNewToken will be called.
|
||||
withContext(Dispatchers.IO) { getToken(APP_ID, TOKEN_SCOPE) }
|
||||
}
|
||||
}
|
5
app/src/huawei/res/values/strings.xml
Normal file
5
app/src/huawei/res/values/strings.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="preferences_notifications_strategy_category_fast_mode_summary">You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers.</string>
|
||||
<string name="activity_pn_mode_fast_mode_explanation">You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers.</string>
|
||||
</resources>
|
@ -310,14 +310,6 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||
</activity>
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.notifications.PushNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
|
@ -41,6 +41,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||
import org.session.libsession.snode.SnodeModule;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
|
||||
import org.session.libsession.utilities.Device;
|
||||
import org.session.libsession.utilities.ProfilePictureUtilities;
|
||||
import org.session.libsession.utilities.SSKEnvironment;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
@ -73,10 +74,9 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
||||
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
|
||||
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.FcmUtils;
|
||||
import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.PushRegistry;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
@ -143,8 +143,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
|
||||
@Inject LokiAPIDatabase lokiAPIDatabase;
|
||||
@Inject public Storage storage;
|
||||
@Inject Device device;
|
||||
@Inject MessageDataProvider messageDataProvider;
|
||||
@Inject TextSecurePreferences textSecurePreferences;
|
||||
@Inject PushRegistry pushRegistry;
|
||||
@Inject ConfigFactory configFactory;
|
||||
CallMessageProcessor callMessageProcessor;
|
||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||
@ -207,8 +209,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
DatabaseModule.init(this);
|
||||
MessagingModuleConfiguration.configure(this);
|
||||
super.onCreate();
|
||||
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
|
||||
messagingModuleConfiguration = new MessagingModuleConfiguration(
|
||||
this,
|
||||
storage,
|
||||
device,
|
||||
messageDataProvider,
|
||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
||||
configFactory
|
||||
@ -226,10 +230,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
broadcaster = new Broadcaster(this);
|
||||
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey != null) {
|
||||
registerForFCMIfNeeded(false);
|
||||
}
|
||||
initializeExpiringMessageManager();
|
||||
initializeTypingStatusRepository();
|
||||
initializeTypingStatusSender();
|
||||
@ -427,33 +427,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
}
|
||||
|
||||
private static class ProviderInitializationException extends RuntimeException { }
|
||||
|
||||
public void registerForFCMIfNeeded(final Boolean force) {
|
||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
|
||||
if (force && firebaseInstanceIdJob != null) {
|
||||
firebaseInstanceIdJob.cancel(null);
|
||||
}
|
||||
firebaseInstanceIdJob = FcmUtils.getFcmInstanceId(task->{
|
||||
if (!task.isSuccessful()) {
|
||||
Log.w("Loki", "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.getException());
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
String token = task.getResult().getToken();
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return Unit.INSTANCE;
|
||||
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
if (TextSecurePreferences.isUsingFCM(this)) {
|
||||
LokiPushNotificationManager.register(token, userPublicKey, this, force);
|
||||
} else {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
}
|
||||
});
|
||||
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpPollingIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return;
|
||||
@ -524,18 +497,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
}
|
||||
|
||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||
String token = TextSecurePreferences.getFCMToken(this);
|
||||
if (token != null && !token.isEmpty()) {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
}
|
||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
|
||||
firebaseInstanceIdJob.cancel(null);
|
||||
}
|
||||
String displayName = TextSecurePreferences.getProfileName(this);
|
||||
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
||||
boolean isUsingFCM = TextSecurePreferences.isPushEnabled(this);
|
||||
TextSecurePreferences.clearAll(this);
|
||||
if (isMigratingToV2KeyPair) {
|
||||
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
||||
TextSecurePreferences.setPushEnabled(this, isUsingFCM);
|
||||
TextSecurePreferences.setProfileName(this, displayName);
|
||||
}
|
||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
|
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 ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
|
||||
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
|
||||
public static final String NOTIFICATION_KEY = "pref_notification_key";
|
||||
public static final String LOKI_SEED = "loki_seed";
|
||||
public static final String HAS_MIGRATED_KEY = "has_migrated_keys";
|
||||
|
||||
|
@ -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.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
@ -591,7 +591,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
val expireTimer = group.disappearingTimer
|
||||
setExpirationTimer(groupId, expireTimer.toInt())
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, group.sessionId, localUserPublicKey)
|
||||
PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey)
|
||||
// Notify the user
|
||||
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||
threadDb.setDate(threadID, formationTimestamp)
|
||||
|
@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.groups
|
||||
import android.content.Context
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
@ -24,7 +24,7 @@ object ClosedGroupManager {
|
||||
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
PushRegistryV1.unsubscribeGroup(closedGroupPublicKey = groupPublicKey, publicKey = userPublicKey)
|
||||
// Stop polling
|
||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||
storage.cancelPendingMessageSendJobs(threadId)
|
||||
|
@ -21,6 +21,7 @@ import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsAdapter
|
||||
@ -31,10 +32,14 @@ import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.fadeIn
|
||||
import org.thoughtcrime.securesms.util.fadeOut
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CreateGroupFragment : Fragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var device: Device
|
||||
|
||||
private lateinit var binding: FragmentCreateGroupBinding
|
||||
private val viewModel: CreateGroupViewModel by viewModels()
|
||||
|
||||
@ -86,7 +91,7 @@ class CreateGroupFragment : Fragment() {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||
isLoading = true
|
||||
binding.loaderContainer.fadeIn()
|
||||
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
MessageSender.createClosedGroup(device, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(requireContext(), Address.fromSerialized(groupID), false))
|
||||
|
@ -67,6 +67,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||
import org.thoughtcrime.securesms.onboarding.SeedActivity
|
||||
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
@ -106,6 +107,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
@Inject lateinit var groupDatabase: GroupDatabase
|
||||
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
||||
@Inject lateinit var configFactory: ConfigFactory
|
||||
@Inject lateinit var pushRegistry: PushRegistry
|
||||
|
||||
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
||||
private val homeViewModel by viewModels<HomeViewModel>()
|
||||
@ -230,8 +232,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
(applicationContext as ApplicationContext).startPollingIfNeeded()
|
||||
// update things based on TextSecurePrefs (profile info etc)
|
||||
// Set up remaining components if needed
|
||||
val application = ApplicationContext.getInstance(this@HomeActivity)
|
||||
application.registerForFCMIfNeeded(false)
|
||||
pushRegistry.refresh(false)
|
||||
if (textSecurePreferences.getLocalNumber() != null) {
|
||||
OpenGroupManager.startPolling()
|
||||
JobQueue.shared.resumePendingJobs()
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import static org.session.libsession.utilities.Util.readFully;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
@ -8,8 +10,6 @@ import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.util.IOUtils;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
||||
@ -148,7 +148,7 @@ public class LinkPreviewRepository {
|
||||
InputStream bodyStream = response.body().byteStream();
|
||||
controller.setStream(bodyStream);
|
||||
|
||||
byte[] data = IOUtils.readInputStreamFully(bodyStream);
|
||||
byte[] data = readFully(bodyStream);
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
Optional<Attachment> thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaTypes.IMAGE_JPEG);
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
@file:JvmName("FcmUtils")
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import com.google.firebase.iid.InstanceIdResult
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
|
||||
fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().launch(Dispatchers.IO) {
|
||||
val task = FirebaseInstanceId.getInstance().instanceId
|
||||
while (!task.isComplete && isActive) {
|
||||
// wait for task to complete while we are active
|
||||
}
|
||||
if (!isActive) return@launch // don't 'complete' task if we were canceled
|
||||
withContext(Dispatchers.Main) {
|
||||
body(task)
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
|
||||
object LokiPushNotificationManager {
|
||||
private val maxRetryCount = 4
|
||||
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
||||
|
||||
private val server by lazy {
|
||||
PushNotificationAPI.server
|
||||
}
|
||||
private val pnServerPublicKey by lazy {
|
||||
PushNotificationAPI.serverPublicKey
|
||||
}
|
||||
|
||||
enum class ClosedGroupOperation {
|
||||
Subscribe, Unsubscribe;
|
||||
|
||||
val rawValue: String
|
||||
get() {
|
||||
return when (this) {
|
||||
Subscribe -> "subscribe_closed_group"
|
||||
Unsubscribe -> "unsubscribe_closed_group"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun unregister(token: String, context: Context) {
|
||||
val parameters = mapOf( "token" to token )
|
||||
val url = "$server/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
getResponseBody(request.build()).map { json ->
|
||||
val code = json["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, false)
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun register(token: String, publicKey: String, context: Context, force: Boolean) {
|
||||
val oldToken = TextSecurePreferences.getFCMToken(context)
|
||||
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
|
||||
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
|
||||
val parameters = mapOf( "token" to token, "pubKey" to publicKey )
|
||||
val url = "$server/register"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
getResponseBody(request.build()).map { json ->
|
||||
val code = json["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, true)
|
||||
TextSecurePreferences.setFCMToken(context, token)
|
||||
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Subscribe to all closed groups
|
||||
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun performOperation(context: Context, operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) {
|
||||
if (!TextSecurePreferences.isUsingFCM(context)) { return }
|
||||
val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey )
|
||||
val url = "$server/${operation.rawValue}"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
getResponseBody(request.build()).map { json ->
|
||||
val code = json["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getResponseBody(request: Request): Promise<Map<*, *>, Exception> {
|
||||
return OnionRequestAPI.sendOnionRequest(request, server, pnServerPublicKey, Version.V2).map { response ->
|
||||
JsonUtil.fromJson(response.body, Map::class.java)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
interface PushManager {
|
||||
fun refresh(force: Boolean)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class PushNotificationService : FirebaseMessagingService() {
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
Log.d("Loki", "New FCM token: $token.")
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return
|
||||
LokiPushNotificationManager.register(token, userPublicKey, this, false)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
Log.d("Loki", "Received a push notification.")
|
||||
val base64EncodedData = message.data?.get("ENCRYPTED_DATA")
|
||||
val data = base64EncodedData?.let { Base64.decode(it) }
|
||||
if (data != null) {
|
||||
try {
|
||||
val envelopeAsData = MessageWrapper.unwrap(data).toByteArray()
|
||||
val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null)
|
||||
JobQueue.shared.add(job)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to unwrap data for message due to error: $e.")
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Failed to decode data for message.")
|
||||
val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER)
|
||||
.setSmallIcon(network.loki.messenger.R.drawable.ic_notification)
|
||||
.setColor(this.getResources().getColor(network.loki.messenger.R.color.textsecure_primary))
|
||||
.setContentTitle("Session")
|
||||
.setContentText("You've got a new message.")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
with(NotificationManagerCompat.from(this)) {
|
||||
notify(11111, builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeletedMessages() {
|
||||
Log.d("Loki", "Called onDeletedMessages.")
|
||||
super.onDeletedMessages()
|
||||
val token = TextSecurePreferences.getFCMToken(this)!!
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return
|
||||
LokiPushNotificationManager.register(token, userPublicKey, this, true)
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.interfaces.AEAD
|
||||
import com.goterl.lazysodium.utils.Key
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.utilities.bencode.Bencode
|
||||
import org.session.libsession.utilities.bencode.BencodeList
|
||||
import org.session.libsession.utilities.bencode.BencodeString
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "PushHandler"
|
||||
|
||||
class PushReceiver @Inject constructor(@ApplicationContext val context: Context) {
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
|
||||
fun onPush(dataMap: Map<String, String>?) {
|
||||
onPush(dataMap?.asByteArray())
|
||||
}
|
||||
|
||||
fun onPush(data: ByteArray?) {
|
||||
if (data == null) {
|
||||
onPush()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val envelopeAsData = MessageWrapper.unwrap(data).toByteArray()
|
||||
val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null)
|
||||
JobQueue.shared.add(job)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to unwrap data for message due to error.", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPush() {
|
||||
Log.d(TAG, "Failed to decode data for message.")
|
||||
val builder = NotificationCompat.Builder(context, NotificationChannels.OTHER)
|
||||
.setSmallIcon(network.loki.messenger.R.drawable.ic_notification)
|
||||
.setColor(context.getColor(network.loki.messenger.R.color.textsecure_primary))
|
||||
.setContentTitle("Session")
|
||||
.setContentText("You've got a new message.")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
NotificationManagerCompat.from(context).notify(11111, builder.build())
|
||||
}
|
||||
|
||||
private fun Map<String, String>.asByteArray() =
|
||||
when {
|
||||
// this is a v2 push notification
|
||||
containsKey("spns") -> {
|
||||
try {
|
||||
decrypt(Base64.decode(this["enc_payload"]))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Invalid push notification", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
// old v1 push notification; we still need this for receiving legacy closed group notifications
|
||||
else -> this["ENCRYPTED_DATA"]?.let(Base64::decode)
|
||||
}
|
||||
|
||||
private fun decrypt(encPayload: ByteArray): ByteArray? {
|
||||
Log.d(TAG, "decrypt() called")
|
||||
|
||||
val encKey = getOrCreateNotificationKey()
|
||||
val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray()
|
||||
val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray()
|
||||
val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce)
|
||||
?: error("Failed to decrypt push notification")
|
||||
val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray()
|
||||
val bencoded = Bencode.Decoder(decrypted)
|
||||
val expectedList = (bencoded.decode() as? BencodeList)?.values
|
||||
?: error("Failed to decode bencoded list from payload")
|
||||
|
||||
val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata")
|
||||
val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson))
|
||||
|
||||
return (expectedList.getOrNull(1) as? BencodeString)?.value.also {
|
||||
// null content is valid only if we got a "data_too_long" flag
|
||||
it?.let { check(metadata.data_len == it.size) { "wrong message data size" } }
|
||||
?: check(metadata.data_too_long) { "missing message data, but no too-long flag" }
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateNotificationKey(): Key {
|
||||
if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) {
|
||||
// generate the key and store it
|
||||
val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF)
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString)
|
||||
}
|
||||
return Key.fromHexString(
|
||||
IdentityKeyUtil.retrieve(
|
||||
context,
|
||||
IdentityKeyUtil.NOTIFICATION_KEY
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.combine.and
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
import org.session.libsignal.utilities.emptyPromise
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private val TAG = PushRegistry::class.java.name
|
||||
|
||||
@Singleton
|
||||
class PushRegistry @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val device: Device,
|
||||
private val tokenManager: TokenManager,
|
||||
private val pushRegistryV2: PushRegistryV2,
|
||||
private val prefs: TextSecurePreferences,
|
||||
private val tokenFetcher: TokenFetcher,
|
||||
) {
|
||||
|
||||
private var pushRegistrationJob: Job? = null
|
||||
|
||||
fun refresh(force: Boolean): Job {
|
||||
Log.d(TAG, "refresh() called with: force = $force")
|
||||
|
||||
pushRegistrationJob?.apply {
|
||||
if (force) cancel() else if (isActive) return MainScope().launch {}
|
||||
}
|
||||
|
||||
return MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
register(tokenFetcher.fetch()).get()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "register failed", e)
|
||||
}
|
||||
}.also { pushRegistrationJob = it }
|
||||
}
|
||||
|
||||
fun register(token: String?): Promise<*, Exception> {
|
||||
Log.d(TAG, "refresh() called")
|
||||
|
||||
if (token?.isNotEmpty() != true) return emptyPromise()
|
||||
|
||||
prefs.setPushToken(token)
|
||||
|
||||
val userPublicKey = prefs.getLocalNumber() ?: return emptyPromise()
|
||||
val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise()
|
||||
|
||||
return when {
|
||||
prefs.isPushEnabled() -> register(token, userPublicKey, userEdKey)
|
||||
tokenManager.isRegistered -> unregister(token, userPublicKey, userEdKey)
|
||||
else -> emptyPromise()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register for push notifications.
|
||||
*/
|
||||
private fun register(
|
||||
token: String,
|
||||
publicKey: String,
|
||||
userEd25519Key: KeyPair,
|
||||
namespaces: List<Int> = listOf(Namespace.DEFAULT)
|
||||
): Promise<*, Exception> {
|
||||
Log.d(TAG, "register() called")
|
||||
|
||||
val v1 = PushRegistryV1.register(
|
||||
device = device,
|
||||
token = token,
|
||||
publicKey = publicKey
|
||||
) fail {
|
||||
Log.e(TAG, "register v1 failed", it)
|
||||
}
|
||||
|
||||
val v2 = pushRegistryV2.register(
|
||||
device, token, publicKey, userEd25519Key, namespaces
|
||||
) fail {
|
||||
Log.e(TAG, "register v2 failed", it)
|
||||
}
|
||||
|
||||
return v1 and v2 success {
|
||||
Log.d(TAG, "register v1 & v2 success")
|
||||
tokenManager.register()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregister(
|
||||
token: String,
|
||||
userPublicKey: String,
|
||||
userEdKey: KeyPair
|
||||
): Promise<*, Exception> = PushRegistryV1.unregister() and pushRegistryV2.unregister(
|
||||
device, token, userPublicKey, userEdKey
|
||||
) fail {
|
||||
Log.e(TAG, "unregisterBoth failed", it)
|
||||
} success {
|
||||
tokenManager.unregister()
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.interfaces.Sign
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.Response
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.Server
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private val TAG = PushRegistryV2::class.java.name
|
||||
private const val maxRetryCount = 4
|
||||
|
||||
@Singleton
|
||||
class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) {
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
|
||||
fun register(
|
||||
device: Device,
|
||||
token: String,
|
||||
publicKey: String,
|
||||
userEd25519Key: KeyPair,
|
||||
namespaces: List<Int>
|
||||
): Promise<SubscriptionResponse, Exception> {
|
||||
val pnKey = pushReceiver.getOrCreateNotificationKey()
|
||||
|
||||
val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s
|
||||
// if we want to support passing namespace list, here is the place to do it
|
||||
val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray()
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes)
|
||||
val requestParameters = SubscriptionRequest(
|
||||
pubkey = publicKey,
|
||||
session_ed25519 = userEd25519Key.publicKey.asHexString,
|
||||
namespaces = listOf(Namespace.DEFAULT),
|
||||
data = true, // only permit data subscription for now (?)
|
||||
service = device.service,
|
||||
sig_ts = timestamp,
|
||||
signature = Base64.encodeBytes(signature),
|
||||
service_info = mapOf("token" to token),
|
||||
enc_key = pnKey.asHexString,
|
||||
).let(Json::encodeToString)
|
||||
|
||||
return retryResponseBody<SubscriptionResponse>("subscribe", requestParameters) success {
|
||||
Log.d(TAG, "registerV2 success")
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(
|
||||
device: Device,
|
||||
token: String,
|
||||
userPublicKey: String,
|
||||
userEdKey: KeyPair
|
||||
): Promise<UnsubscribeResponse, Exception> {
|
||||
val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s
|
||||
// if we want to support passing namespace list, here is the place to do it
|
||||
val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray()
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes)
|
||||
|
||||
val requestParameters = UnsubscriptionRequest(
|
||||
pubkey = userPublicKey,
|
||||
session_ed25519 = userEdKey.publicKey.asHexString,
|
||||
service = device.service,
|
||||
sig_ts = timestamp,
|
||||
signature = Base64.encodeBytes(signature),
|
||||
service_info = mapOf("token" to token),
|
||||
).let(Json::encodeToString)
|
||||
|
||||
return retryResponseBody<UnsubscribeResponse>("unsubscribe", requestParameters) success {
|
||||
Log.d(TAG, "unregisterV2 success")
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T: Response> retryResponseBody(path: String, requestParameters: String): Promise<T, Exception> =
|
||||
retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) }
|
||||
|
||||
private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
|
||||
val server = Server.LATEST
|
||||
val url = "${server.url}/$path"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), requestParameters)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V4
|
||||
).map { response ->
|
||||
response.body!!.inputStream()
|
||||
.let { Json.decodeFromStream<T>(it) }
|
||||
.also { if (it.isFailure()) throw Exception("error: ${it.message}.") }
|
||||
}
|
||||
}
|
||||
}
|
@ -99,7 +99,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
||||
.get();
|
||||
setLargeIcon(iconBitmap);
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
Log.w(TAG, "get iconBitmap in getThread failed", e);
|
||||
setLargeIcon(getPlaceholderDrawable(context, recipient));
|
||||
}
|
||||
} else {
|
||||
@ -298,7 +298,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
||||
.submit(64, 64)
|
||||
.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
Log.w(TAG, "getBigPicture failed", e);
|
||||
return Bitmap.createBitmap(64, 64, Bitmap.Config.RGB_565);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
interface TokenFetcher {
|
||||
suspend fun fetch(): String?
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val INTERVAL: Int = 12 * 60 * 60 * 1000
|
||||
|
||||
@Singleton
|
||||
class TokenManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
val hasValidRegistration get() = isRegistered && !isExpired
|
||||
val isRegistered get() = time > 0
|
||||
private val isExpired get() = currentTime() > time + INTERVAL
|
||||
|
||||
fun register() {
|
||||
time = currentTime()
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
time = 0
|
||||
}
|
||||
|
||||
private var time
|
||||
get() = TextSecurePreferences.getPushRegisterTime(context)
|
||||
set(value) = TextSecurePreferences.setPushRegisterTime(context, value)
|
||||
|
||||
private fun currentTime() = System.currentTimeMillis()
|
||||
}
|
@ -12,6 +12,7 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityPnModeBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
@ -19,6 +20,8 @@ import org.session.libsession.utilities.ThemeUtil
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.home.HomeActivity
|
||||
import org.thoughtcrime.securesms.notifications.PushManager
|
||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.PNModeView
|
||||
@ -27,8 +30,13 @@ import org.thoughtcrime.securesms.util.getAccentColor
|
||||
import org.thoughtcrime.securesms.util.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PNModeActivity : BaseActionBarActivity() {
|
||||
|
||||
@Inject lateinit var pushRegistry: PushRegistry
|
||||
|
||||
private lateinit var binding: ActivityPnModeBinding
|
||||
private var selectedOptionView: PNModeView? = null
|
||||
|
||||
@ -158,10 +166,10 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
|
||||
TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView))
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.startPollingIfNeeded()
|
||||
application.registerForFCMIfNeeded(true)
|
||||
pushRegistry.refresh(true)
|
||||
val intent = Intent(this, HomeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
intent.putExtra(HomeActivity.FROM_ONBOARDING, true)
|
||||
|
@ -1,10 +1,12 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.os.Bundle
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
|
@ -1,182 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
import static org.thoughtcrime.securesms.preferences.ListPreferenceDialogKt.listPreferenceDialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
// Set up FCM toggle
|
||||
String fcmKey = "pref_key_use_fcm";
|
||||
((SwitchPreferenceCompat)findPreference(fcmKey)).setChecked(TextSecurePreferences.isUsingFCM(getContext()));
|
||||
this.findPreference(fcmKey)
|
||||
.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue);
|
||||
ApplicationContext.getInstance(getContext()).registerForFCMIfNeeded(true);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
TextSecurePreferences.setNotificationRingtone(getContext(), NotificationChannels.getMessageRingtone(getContext()).toString());
|
||||
TextSecurePreferences.setNotificationVibrateEnabled(getContext(), NotificationChannels.getMessageVibrate(getContext()));
|
||||
}
|
||||
this.findPreference(TextSecurePreferences.RINGTONE_PREF)
|
||||
.setOnPreferenceChangeListener(new RingtoneSummaryListener());
|
||||
this.findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)
|
||||
.setOnPreferenceChangeListener(new NotificationPrivacyListener());
|
||||
this.findPreference(TextSecurePreferences.VIBRATE_PREF)
|
||||
.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
NotificationChannels.updateMessageVibrate(getContext(), (boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
this.findPreference(TextSecurePreferences.RINGTONE_PREF)
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
Uri current = TextSecurePreferences.getNotificationRingtone(getContext());
|
||||
|
||||
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
|
||||
|
||||
startActivityForResult(intent, 1);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
ListPreference listPreference = (ListPreference) preference;
|
||||
listPreference.setDialogMessage(R.string.preferences_notifications__content_message);
|
||||
listPreferenceDialog(getContext(), listPreference, () -> {
|
||||
initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF));
|
||||
return null;
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF));
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
this.findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(getContext()));
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF));
|
||||
initializeMessageVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.VIBRATE_PREF));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences_notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == 1 && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
|
||||
|
||||
if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) {
|
||||
NotificationChannels.updateMessageRingtone(getContext(), uri);
|
||||
TextSecurePreferences.removeNotificationRingtone(getContext());
|
||||
} else {
|
||||
uri = uri == null ? Uri.EMPTY : uri;
|
||||
NotificationChannels.updateMessageRingtone(getContext(), uri);
|
||||
TextSecurePreferences.setNotificationRingtone(getContext(), uri.toString());
|
||||
}
|
||||
|
||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF));
|
||||
}
|
||||
}
|
||||
|
||||
private class RingtoneSummaryListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
Uri value = (Uri) newValue;
|
||||
|
||||
if (value == null || TextUtils.isEmpty(value.toString())) {
|
||||
preference.setSummary(R.string.preferences__silent);
|
||||
} else {
|
||||
Ringtone tone = RingtoneManager.getRingtone(getActivity(), value);
|
||||
|
||||
if (tone != null) {
|
||||
preference.setSummary(tone.getTitle(getActivity()));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeRingtoneSummary(Preference pref) {
|
||||
RingtoneSummaryListener listener = (RingtoneSummaryListener) pref.getOnPreferenceChangeListener();
|
||||
Uri uri = TextSecurePreferences.getNotificationRingtone(getContext());
|
||||
|
||||
listener.onPreferenceChange(pref, uri);
|
||||
}
|
||||
|
||||
private void initializeMessageVibrateSummary(SwitchPreferenceCompat pref) {
|
||||
pref.setChecked(TextSecurePreferences.isNotificationVibrateEnabled(getContext()));
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
final int onCapsResId = R.string.ApplicationPreferencesActivity_On;
|
||||
final int offCapsResId = R.string.ApplicationPreferencesActivity_Off;
|
||||
|
||||
return context.getString(TextSecurePreferences.isNotificationsEnabled(context) ? onCapsResId : offCapsResId);
|
||||
}
|
||||
|
||||
private class NotificationPrivacyListener extends ListSummaryListener {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object value) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
ApplicationContext.getInstance(getActivity()).messageNotifier.updateNotification(getActivity());
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
return super.onPreferenceChange(preference, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.isNotificationsEnabled
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() {
|
||||
@Inject
|
||||
lateinit var pushRegistry: PushRegistry
|
||||
@Inject
|
||||
lateinit var prefs: TextSecurePreferences
|
||||
|
||||
override fun onCreate(paramBundle: Bundle?) {
|
||||
super.onCreate(paramBundle)
|
||||
|
||||
// Set up FCM toggle
|
||||
val fcmKey = "pref_key_use_fcm"
|
||||
val fcmPreference: SwitchPreferenceCompat = findPreference(fcmKey)!!
|
||||
fcmPreference.isChecked = prefs.isPushEnabled()
|
||||
fcmPreference.setOnPreferenceChangeListener { _: Preference, newValue: Any ->
|
||||
prefs.setPushEnabled(newValue as Boolean)
|
||||
val job = pushRegistry.refresh(true)
|
||||
|
||||
fcmPreference.isEnabled = false
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
job.join()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
fcmPreference.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
if (NotificationChannels.supported()) {
|
||||
prefs.setNotificationRingtone(
|
||||
NotificationChannels.getMessageRingtone(requireContext()).toString()
|
||||
)
|
||||
prefs.setNotificationVibrateEnabled(
|
||||
NotificationChannels.getMessageVibrate(requireContext())
|
||||
)
|
||||
}
|
||||
findPreference<Preference>(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceChangeListener = RingtoneSummaryListener()
|
||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceChangeListener = NotificationPrivacyListener()
|
||||
findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF)!!.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
||||
NotificationChannels.updateMessageVibrate(requireContext(), newValue as Boolean)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
val current = prefs.getNotificationRingtone()
|
||||
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
|
||||
intent.putExtra(
|
||||
RingtoneManager.EXTRA_RINGTONE_TYPE,
|
||||
RingtoneManager.TYPE_NOTIFICATION
|
||||
)
|
||||
intent.putExtra(
|
||||
RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI,
|
||||
Settings.System.DEFAULT_NOTIFICATION_URI
|
||||
)
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current)
|
||||
startActivityForResult(intent, 1)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener { preference: Preference ->
|
||||
val listPreference = preference as ListPreference
|
||||
listPreference.setDialogMessage(R.string.preferences_notifications__content_message)
|
||||
listPreferenceDialog(requireContext(), listPreference) {
|
||||
initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF))
|
||||
}
|
||||
true
|
||||
}
|
||||
initializeListSummary(findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) as ListPreference?)
|
||||
if (NotificationChannels.supported()) {
|
||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(
|
||||
Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(requireContext())
|
||||
)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
}
|
||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF))
|
||||
initializeMessageVibrateSummary(findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF) as SwitchPreferenceCompat?)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_notifications)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == 1 && resultCode == Activity.RESULT_OK && data != null) {
|
||||
var uri = data.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||
if (Settings.System.DEFAULT_NOTIFICATION_URI == uri) {
|
||||
NotificationChannels.updateMessageRingtone(requireContext(), uri)
|
||||
prefs.removeNotificationRingtone()
|
||||
} else {
|
||||
uri = uri ?: Uri.EMPTY
|
||||
NotificationChannels.updateMessageRingtone(requireContext(), uri)
|
||||
prefs.setNotificationRingtone(uri.toString())
|
||||
}
|
||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RingtoneSummaryListener : Preference.OnPreferenceChangeListener {
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
val value = newValue as? Uri
|
||||
if (value == null || TextUtils.isEmpty(value.toString())) {
|
||||
preference.setSummary(R.string.preferences__silent)
|
||||
} else {
|
||||
RingtoneManager.getRingtone(activity, value)
|
||||
?.getTitle(activity)
|
||||
?.let { preference.summary = it }
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeRingtoneSummary(pref: Preference?) {
|
||||
val listener = pref!!.onPreferenceChangeListener as RingtoneSummaryListener?
|
||||
val uri = prefs.getNotificationRingtone()
|
||||
listener!!.onPreferenceChange(pref, uri)
|
||||
}
|
||||
|
||||
private fun initializeMessageVibrateSummary(pref: SwitchPreferenceCompat?) {
|
||||
pref!!.isChecked = prefs.isNotificationVibrateEnabled()
|
||||
}
|
||||
|
||||
private inner class NotificationPrivacyListener : ListSummaryListener() {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
override fun onPreferenceChange(preference: Preference, value: Any): Boolean {
|
||||
object : AsyncTask<Void?, Void?, Void?>() {
|
||||
override fun doInBackground(vararg params: Void?): Void? {
|
||||
ApplicationContext.getInstance(activity).messageNotifier.updateNotification(activity!!)
|
||||
return null
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||
return super.onPreferenceChange(preference, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("unused")
|
||||
private val TAG = NotificationsPreferenceFragment::class.java.simpleName
|
||||
fun getSummary(context: Context): CharSequence = when (isNotificationsEnabled(context)) {
|
||||
true -> R.string.ApplicationPreferencesActivity_On
|
||||
false -> R.string.ApplicationPreferencesActivity_Off
|
||||
}.let(context::getString)
|
||||
}
|
||||
}
|
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 {
|
||||
google()
|
||||
mavenCentral()
|
||||
if (project.hasProperty('huawei')) maven {
|
||||
url 'https://developer.huawei.com/repo/'
|
||||
content {
|
||||
includeGroup 'com.huawei.agconnect'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||
classpath "com.google.gms:google-services:$googleServicesVersion"
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
classpath "com.squareup:javapoet:1.13.0"
|
||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
||||
if (project.hasProperty('huawei')) classpath 'com.huawei.agconnect:agcp:1.9.1.300'
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +63,15 @@ allprojects {
|
||||
}
|
||||
jcenter()
|
||||
maven { url "https://jitpack.io" }
|
||||
if (project.hasProperty('huawei')) maven {
|
||||
url 'https://developer.huawei.com/repo/'
|
||||
content {
|
||||
includeGroup 'com.huawei.android.hms'
|
||||
includeGroup 'com.huawei.agconnect'
|
||||
includeGroup 'com.huawei.hmf'
|
||||
includeGroup 'com.huawei.hms'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.ext {
|
||||
|
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
id 'kotlinx-serialization'
|
||||
}
|
||||
|
||||
android {
|
||||
@ -41,6 +42,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
|
@ -5,10 +5,12 @@ import com.goterl.lazysodium.utils.KeyPair
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsession.utilities.Device
|
||||
|
||||
class MessagingModuleConfiguration(
|
||||
val context: Context,
|
||||
val storage: StorageProtocol,
|
||||
val device: Device,
|
||||
val messageDataProvider: MessageDataProvider,
|
||||
val getUserED25519KeyPair: () -> KeyPair?,
|
||||
val configFactory: ConfigFactoryProtocol
|
||||
|
@ -3,12 +3,11 @@ package org.session.libsession.messaging.jobs
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.Server
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
@ -31,23 +30,27 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
||||
}
|
||||
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val server = PushNotificationAPI.server
|
||||
val server = Server.LEGACY
|
||||
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
|
||||
val url = "${server}/notify"
|
||||
val url = "${server.url}/notify"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
retryIfNeeded(4) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V2
|
||||
) success { response ->
|
||||
when (response.code) {
|
||||
null, 0 -> Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: ${response.message}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
||||
} fail { exception ->
|
||||
Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: $exception.")
|
||||
}
|
||||
}.success {
|
||||
} success {
|
||||
handleSuccess(dispatcherName)
|
||||
}. fail {
|
||||
} fail {
|
||||
handleFailure(dispatcherName, it)
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.crypto.PushTransportDetails
|
||||
@ -454,8 +455,8 @@ object MessageSender {
|
||||
}
|
||||
|
||||
// Closed groups
|
||||
fun createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
return create(name, members)
|
||||
fun createClosedGroup(device: Device, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
return create(device, name, members)
|
||||
}
|
||||
|
||||
fun explicitNameChange(groupPublicKey: String, newName: String) {
|
||||
|
@ -8,11 +8,12 @@ import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
@ -33,7 +34,11 @@ const val groupSizeLimit = 100
|
||||
|
||||
val pendingKeyPairs = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
fun MessageSender.create(
|
||||
device: Device,
|
||||
name: String,
|
||||
members: Collection<String>
|
||||
): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
@ -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
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
PushRegistryV1.register(device = device, publicKey = userPublicKey)
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||
// Fulfill the promise
|
||||
|
@ -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.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
@ -556,10 +556,14 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
||||
// Set expiration timer
|
||||
storage.setExpirationTimer(groupID, expireTimer)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Create thread
|
||||
val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.setThreadDate(threadId, formationTimestamp)
|
||||
PushRegistryV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey)
|
||||
// Notify the user
|
||||
if (userPublicKey == sender && !groupExists) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
||||
} else if (userPublicKey != sender) {
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
|
||||
}
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||
}
|
||||
@ -865,7 +869,7 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou
|
||||
storage.setActive(groupID, false)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
PushRegistryV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey)
|
||||
// Stop polling
|
||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||
|
||||
|
@ -14,4 +14,4 @@ interface MessageNotifier {
|
||||
fun updateNotification(context: Context, threadId: Long, signal: Boolean)
|
||||
fun updateNotification(context: Context, signal: Boolean, reminderCount: Int)
|
||||
fun clearReminder(context: Context)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,126 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import com.goterl.lazysodium.utils.Key
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server.
|
||||
* Changing the variable names will break how data is serialized/deserialized.
|
||||
* If it's less than ideally named we can use [SerialName], such as for the push metadata which uses
|
||||
* single-letter keys to be as compact as possible.
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionRequest(
|
||||
/** the 33-byte account being subscribed to; typically a session ID */
|
||||
val pubkey: String,
|
||||
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
|
||||
val session_ed25519: String?,
|
||||
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
|
||||
val subkey_tag: String? = null,
|
||||
/** array of integer namespaces to subscribe to, **must be sorted in ascending order** */
|
||||
val namespaces: List<Int>,
|
||||
/** if provided and true then notifications will include the body of the message (as long as it isn't too large) */
|
||||
val data: Boolean,
|
||||
/** the signature unix timestamp in seconds, not ms */
|
||||
val sig_ts: Long,
|
||||
/** the 64-byte ed25519 signature */
|
||||
val signature: String,
|
||||
/** the string identifying the notification service, "firebase" for android (currently) */
|
||||
val service: String,
|
||||
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
|
||||
val service_info: Map<String, String>,
|
||||
/** 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 via libsodium using this key.
|
||||
* persist it on device */
|
||||
val enc_key: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UnsubscriptionRequest(
|
||||
/** the 33-byte account being subscribed to; typically a session ID */
|
||||
val pubkey: String,
|
||||
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
|
||||
val session_ed25519: String?,
|
||||
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
|
||||
val subkey_tag: String? = null,
|
||||
/** the signature unix timestamp in seconds, not ms */
|
||||
val sig_ts: Long,
|
||||
/** the 64-byte ed25519 signature */
|
||||
val signature: String,
|
||||
/** the string identifying the notification service, "firebase" for android (currently) */
|
||||
val service: String,
|
||||
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
|
||||
val service_info: Map<String, String>,
|
||||
)
|
||||
|
||||
/** invalid values, missing reuqired arguments etc, details in message */
|
||||
private const val UNPARSEABLE_ERROR = 1
|
||||
/** the "service" value is not active / valid */
|
||||
private const val SERVICE_NOT_AVAILABLE = 2
|
||||
/** something getting wrong internally talking to the backend */
|
||||
private const val SERVICE_TIMEOUT = 3
|
||||
/** other error processing the subscription (details in the message) */
|
||||
private const val GENERIC_ERROR = 4
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionResponse(
|
||||
override val error: Int? = null,
|
||||
override val message: String? = null,
|
||||
override val success: Boolean? = null,
|
||||
val added: Boolean? = null,
|
||||
val updated: Boolean? = null,
|
||||
): Response
|
||||
|
||||
@Serializable
|
||||
data class UnsubscribeResponse(
|
||||
override val error: Int? = null,
|
||||
override val message: String? = null,
|
||||
override val success: Boolean? = null,
|
||||
val removed: Boolean? = null,
|
||||
): Response
|
||||
|
||||
interface Response {
|
||||
val error: Int?
|
||||
val message: String?
|
||||
val success: Boolean?
|
||||
fun isSuccess() = success == true && error == null
|
||||
fun isFailure() = !isSuccess()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationMetadata(
|
||||
/** Account ID (such as Session ID or closed group ID) where the message arrived **/
|
||||
@SerialName("@")
|
||||
val account: String,
|
||||
|
||||
/** The hash of the message in the swarm. */
|
||||
@SerialName("#")
|
||||
val msg_hash: String,
|
||||
|
||||
/** The swarm namespace in which this message arrived. */
|
||||
@SerialName("n")
|
||||
val namespace: Int,
|
||||
|
||||
/** The length of the message data. This is always included, even if the message content
|
||||
* itself was too large to fit into the push notification. */
|
||||
@SerialName("l")
|
||||
val data_len: Int,
|
||||
|
||||
/** This will be true if the data was omitted because it was too long to fit in a push
|
||||
* notification (around 2.5kB of raw data), in which case the push notification includes
|
||||
* only this metadata but not the message content itself. */
|
||||
@SerialName("B")
|
||||
val data_too_long : Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationServerObject(
|
||||
val enc_payload: String,
|
||||
val spns: Int,
|
||||
) {
|
||||
fun decryptPayload(key: Key): Any {
|
||||
|
||||
TODO()
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushNotificationAPI {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val server = "https://live.apns.getsession.org"
|
||||
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
private val maxRetryCount = 4
|
||||
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
||||
|
||||
enum class ClosedGroupOperation {
|
||||
Subscribe, Unsubscribe;
|
||||
|
||||
val rawValue: String
|
||||
get() {
|
||||
return when (this) {
|
||||
Subscribe -> "subscribe_closed_group"
|
||||
Unsubscribe -> "unsubscribe_closed_group"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(token: String) {
|
||||
val parameters = mapOf( "token" to token )
|
||||
val url = "$server/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, false)
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun register(token: String, publicKey: String, force: Boolean) {
|
||||
val oldToken = TextSecurePreferences.getFCMToken(context)
|
||||
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
|
||||
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
|
||||
val parameters = mapOf( "token" to token, "pubKey" to publicKey )
|
||||
val url = "$server/register"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, true)
|
||||
TextSecurePreferences.setFCMToken(context, token)
|
||||
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Subscribe to all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun performOperation(operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) {
|
||||
if (!TextSecurePreferences.isUsingFCM(context)) { return }
|
||||
val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey )
|
||||
val url = "$server/${operation.rawValue}"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.Promise
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.OnionResponse
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.emptyPromise
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.sideEffect
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushRegistryV1 {
|
||||
private val TAG = PushRegistryV1::class.java.name
|
||||
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
private const val maxRetryCount = 4
|
||||
|
||||
private val server = Server.LEGACY
|
||||
|
||||
fun register(
|
||||
device: Device,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
token: String? = TextSecurePreferences.getPushToken(context),
|
||||
publicKey: String? = TextSecurePreferences.getLocalNumber(context),
|
||||
legacyGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
): Promise<*, Exception> = when {
|
||||
isPushEnabled -> retryIfNeeded(maxRetryCount) {
|
||||
Log.d(TAG, "register() called")
|
||||
doRegister(token, publicKey, device, legacyGroupPublicKeys)
|
||||
} fail { exception ->
|
||||
Log.d(TAG, "Couldn't register for FCM due to error", exception)
|
||||
}
|
||||
|
||||
else -> emptyPromise()
|
||||
}
|
||||
|
||||
private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection<String>): Promise<*, Exception> {
|
||||
Log.d(TAG, "doRegister() called")
|
||||
|
||||
token ?: return emptyPromise()
|
||||
publicKey ?: return emptyPromise()
|
||||
|
||||
val parameters = mapOf(
|
||||
"token" to token,
|
||||
"pubKey" to publicKey,
|
||||
"device" to device.value,
|
||||
"legacyGroupPublicKeys" to legacyGroupPublicKeys
|
||||
)
|
||||
|
||||
val url = "${server.url}/register_legacy_groups_only"
|
||||
val body = RequestBody.create(
|
||||
MediaType.get("application/json"),
|
||||
JsonUtil.toJson(parameters)
|
||||
)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return sendOnionRequest(request) sideEffect { response ->
|
||||
when (response.code) {
|
||||
null, 0 -> throw Exception("error: ${response.message}.")
|
||||
}
|
||||
} success {
|
||||
Log.d(TAG, "registerV1 success")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager.
|
||||
*/
|
||||
fun unregister(): Promise<*, Exception> {
|
||||
Log.d(TAG, "unregisterV1 requested")
|
||||
|
||||
val token = TextSecurePreferences.getPushToken(context) ?: emptyPromise()
|
||||
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val parameters = mapOf("token" to token)
|
||||
val url = "${server.url}/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
sendOnionRequest(request) success {
|
||||
when (it.code) {
|
||||
null, 0 -> Log.d(TAG, "error: ${it.message}.")
|
||||
else -> Log.d(TAG, "unregisterV1 success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy Closed Groups
|
||||
|
||||
fun subscribeGroup(
|
||||
closedGroupPublicKey: String,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
) = if (isPushEnabled) {
|
||||
performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey)
|
||||
} else emptyPromise()
|
||||
|
||||
fun unsubscribeGroup(
|
||||
closedGroupPublicKey: String,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
) = if (isPushEnabled) {
|
||||
performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey)
|
||||
} else emptyPromise()
|
||||
|
||||
private fun performGroupOperation(
|
||||
operation: String,
|
||||
closedGroupPublicKey: String,
|
||||
publicKey: String
|
||||
): Promise<*, Exception> {
|
||||
val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey)
|
||||
val url = "${server.url}/$operation"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
sendOnionRequest(request) sideEffect {
|
||||
when (it.code) {
|
||||
0, null -> throw Exception(it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOnionRequest(request: Request): Promise<OnionResponse, Exception> = OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V2
|
||||
)
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
enum class Server(val url: String, val publicKey: String) {
|
||||
LATEST("https://push.getsession.org", "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"),
|
||||
LEGACY("https://live.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049")
|
||||
}
|
@ -205,7 +205,7 @@ object SodiumUtilities {
|
||||
}
|
||||
|
||||
fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? {
|
||||
val plaintextSize = ciphertext.size - AEAD.CHACHA20POLY1305_ABYTES
|
||||
val plaintextSize = ciphertext.size - AEAD.XCHACHA20POLY1305_IETF_ABYTES
|
||||
val plaintext = ByteArray(plaintextSize)
|
||||
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||
plaintext,
|
||||
|
@ -686,4 +686,7 @@ enum class Version(val value: String) {
|
||||
data class OnionResponse(
|
||||
val info: Map<*, *>,
|
||||
val body: ByteArray? = null
|
||||
)
|
||||
) {
|
||||
val code: Int? get() = info["code"] as? Int
|
||||
val message: String? get() = info["message"] as? String
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
enum class Device(val value: String, val service: String = value) {
|
||||
ANDROID("android", "firebase"),
|
||||
HUAWEI("huawei");
|
||||
}
|
@ -37,12 +37,12 @@ interface TextSecurePreferences {
|
||||
fun setLastConfigurationSyncTime(value: Long)
|
||||
fun getConfigurationMessageSynced(): Boolean
|
||||
fun setConfigurationMessageSynced(value: Boolean)
|
||||
fun isUsingFCM(): Boolean
|
||||
fun setIsUsingFCM(value: Boolean)
|
||||
fun getFCMToken(): String?
|
||||
fun setFCMToken(value: String)
|
||||
fun getLastFCMUploadTime(): Long
|
||||
fun setLastFCMUploadTime(value: Long)
|
||||
fun isPushEnabled(): Boolean
|
||||
fun setPushEnabled(value: Boolean)
|
||||
fun getPushToken(): String?
|
||||
fun setPushToken(value: String)
|
||||
fun getPushRegisterTime(): Long
|
||||
fun setPushRegisterTime(value: Long)
|
||||
fun isScreenLockEnabled(): Boolean
|
||||
fun setScreenLockEnabled(value: Boolean)
|
||||
fun getScreenLockTimeout(): Long
|
||||
@ -251,9 +251,9 @@ interface TextSecurePreferences {
|
||||
const val LINK_PREVIEWS = "pref_link_previews"
|
||||
const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning"
|
||||
const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
|
||||
const val IS_USING_FCM = "pref_is_using_fcm"
|
||||
const val FCM_TOKEN = "pref_fcm_token"
|
||||
const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2"
|
||||
const val IS_PUSH_ENABLED = "pref_is_using_fcm"
|
||||
const val PUSH_TOKEN = "pref_fcm_token_2"
|
||||
const val PUSH_REGISTER_TIME = "pref_last_fcm_token_upload_time_2"
|
||||
const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
|
||||
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
||||
const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
|
||||
@ -309,31 +309,31 @@ interface TextSecurePreferences {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUsingFCM(context: Context): Boolean {
|
||||
return getBooleanPreference(context, IS_USING_FCM, false)
|
||||
fun isPushEnabled(context: Context): Boolean {
|
||||
return getBooleanPreference(context, IS_PUSH_ENABLED, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setIsUsingFCM(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, IS_USING_FCM, value)
|
||||
fun setPushEnabled(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, IS_PUSH_ENABLED, value)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getFCMToken(context: Context): String? {
|
||||
return getStringPreference(context, FCM_TOKEN, "")
|
||||
fun getPushToken(context: Context): String? {
|
||||
return getStringPreference(context, PUSH_TOKEN, "")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setFCMToken(context: Context, value: String) {
|
||||
setStringPreference(context, FCM_TOKEN, value)
|
||||
fun setPushToken(context: Context, value: String?) {
|
||||
setStringPreference(context, PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
fun getLastFCMUploadTime(context: Context): Long {
|
||||
return getLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, 0)
|
||||
fun getPushRegisterTime(context: Context): Long {
|
||||
return getLongPreference(context, PUSH_REGISTER_TIME, 0)
|
||||
}
|
||||
|
||||
fun setLastFCMUploadTime(context: Context, value: Long) {
|
||||
setLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, value)
|
||||
fun setPushRegisterTime(context: Context, value: Long) {
|
||||
setLongPreference(context, PUSH_REGISTER_TIME, value)
|
||||
}
|
||||
|
||||
// endregion
|
||||
@ -1008,7 +1008,6 @@ interface TextSecurePreferences {
|
||||
fun clearAll(context: Context) {
|
||||
getDefaultSharedPreferences(context).edit().clear().commit()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1033,28 +1032,28 @@ class AppTextSecurePreferences @Inject constructor(
|
||||
TextSecurePreferences._events.tryEmit(TextSecurePreferences.CONFIGURATION_SYNCED)
|
||||
}
|
||||
|
||||
override fun isUsingFCM(): Boolean {
|
||||
return getBooleanPreference(TextSecurePreferences.IS_USING_FCM, false)
|
||||
override fun isPushEnabled(): Boolean {
|
||||
return getBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, false)
|
||||
}
|
||||
|
||||
override fun setIsUsingFCM(value: Boolean) {
|
||||
setBooleanPreference(TextSecurePreferences.IS_USING_FCM, value)
|
||||
override fun setPushEnabled(value: Boolean) {
|
||||
setBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, value)
|
||||
}
|
||||
|
||||
override fun getFCMToken(): String? {
|
||||
return getStringPreference(TextSecurePreferences.FCM_TOKEN, "")
|
||||
override fun getPushToken(): String? {
|
||||
return getStringPreference(TextSecurePreferences.PUSH_TOKEN, "")
|
||||
}
|
||||
|
||||
override fun setFCMToken(value: String) {
|
||||
setStringPreference(TextSecurePreferences.FCM_TOKEN, value)
|
||||
override fun setPushToken(value: String) {
|
||||
setStringPreference(TextSecurePreferences.PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
override fun getLastFCMUploadTime(): Long {
|
||||
return getLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, 0)
|
||||
override fun getPushRegisterTime(): Long {
|
||||
return getLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, 0)
|
||||
}
|
||||
|
||||
override fun setLastFCMUploadTime(value: Long) {
|
||||
setLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, value)
|
||||
override fun setPushRegisterTime(value: Long) {
|
||||
setLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, value)
|
||||
}
|
||||
|
||||
override fun isScreenLockEnabled(): Boolean {
|
||||
|
@ -0,0 +1,169 @@
|
||||
package org.session.libsession.utilities.bencode
|
||||
|
||||
import java.util.LinkedList
|
||||
|
||||
object Bencode {
|
||||
class Decoder(source: ByteArray) {
|
||||
|
||||
private val iterator = LinkedList<Byte>().apply {
|
||||
addAll(source.asIterable())
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an element based on next marker assumed to be string/int/list/dict or return null
|
||||
*/
|
||||
fun decode(): BencodeElement? {
|
||||
val result = when (iterator.peek()?.toInt()?.toChar()) {
|
||||
in NUMBERS -> decodeString()
|
||||
INT_INDICATOR -> decodeInt()
|
||||
LIST_INDICATOR -> decodeList()
|
||||
DICT_INDICATOR -> decodeDict()
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a string element from iterator assumed to have structure `{length}:{data}`
|
||||
*/
|
||||
private fun decodeString(): BencodeString? {
|
||||
val lengthStrings = buildString {
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != SEPARATOR) {
|
||||
append(iterator.pop().toInt().toChar())
|
||||
}
|
||||
}
|
||||
iterator.pop() // drop `:`
|
||||
val length = lengthStrings.toIntOrNull(10) ?: return null
|
||||
val remaining = (0 until length).map { iterator.pop() }.toByteArray()
|
||||
return BencodeString(remaining)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an int element from iterator assumed to have structure `i{int}e`
|
||||
*/
|
||||
private fun decodeInt(): BencodeElement? {
|
||||
iterator.pop() // drop `i`
|
||||
val intString = buildString {
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
append(iterator.pop().toInt().toChar())
|
||||
}
|
||||
}
|
||||
val asInt = intString.toIntOrNull(10) ?: return null
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeInteger(asInt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a list element from iterator assumed to have structure `l{data}e`
|
||||
*/
|
||||
private fun decodeList(): BencodeElement {
|
||||
iterator.pop() // drop `l`
|
||||
val listElements = mutableListOf<BencodeElement>()
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
decode()?.let { nextElement ->
|
||||
listElements += nextElement
|
||||
}
|
||||
}
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeList(listElements)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a dict element from iterator assumed to have structure `d{data}e`
|
||||
*/
|
||||
private fun decodeDict(): BencodeElement? {
|
||||
iterator.pop() // drop `d`
|
||||
val dictElements = mutableMapOf<String,BencodeElement>()
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
val key = decodeString() ?: return null
|
||||
val value = decode() ?: return null
|
||||
dictElements += key.value.decodeToString() to value
|
||||
}
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeDict(dictElements)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NUMBERS = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
|
||||
private const val INT_INDICATOR = 'i'
|
||||
private const val LIST_INDICATOR = 'l'
|
||||
private const val DICT_INDICATOR = 'd'
|
||||
private const val END_INDICATOR = 'e'
|
||||
private const val SEPARATOR = ':'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class BencodeElement {
|
||||
abstract fun encode(): ByteArray
|
||||
}
|
||||
|
||||
fun String.bencode() = BencodeString(this.encodeToByteArray())
|
||||
fun Int.bencode() = BencodeInteger(this)
|
||||
|
||||
data class BencodeString(val value: ByteArray): BencodeElement() {
|
||||
override fun encode(): ByteArray = buildString {
|
||||
append(value.size.toString())
|
||||
append(':')
|
||||
}.toByteArray() + value
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BencodeString
|
||||
|
||||
if (!value.contentEquals(other.value)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.contentHashCode()
|
||||
}
|
||||
}
|
||||
data class BencodeInteger(val value: Int): BencodeElement() {
|
||||
override fun encode(): ByteArray = buildString {
|
||||
append('i')
|
||||
append(value.toString())
|
||||
append('e')
|
||||
}.toByteArray()
|
||||
}
|
||||
data class BencodeList(val values: List<BencodeElement>): BencodeElement() {
|
||||
|
||||
constructor(vararg values: BencodeElement) : this(values.toList())
|
||||
|
||||
override fun encode(): ByteArray = "l".toByteArray() +
|
||||
values.fold(byteArrayOf()) { array, element -> array + element.encode() } +
|
||||
"e".toByteArray()
|
||||
}
|
||||
data class BencodeDict(val values: Map<String, BencodeElement>): BencodeElement() {
|
||||
|
||||
constructor(vararg values: Pair<String, BencodeElement>) : this(values.toMap())
|
||||
|
||||
override fun encode(): ByteArray = "d".toByteArray() +
|
||||
values.entries.fold(byteArrayOf()) { array, (key, value) ->
|
||||
array + key.bencode().encode() + value.encode()
|
||||
} + "e".toByteArray()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BencodeDict
|
||||
|
||||
if (values != other.values) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return values.hashCode()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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)
|
||||
fun getDir(context: Context, type: String?): File {
|
||||
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)
|
||||
@ -73,10 +73,7 @@ object ExternalStorageUtil {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getCleanFileName(fileName: String?): String? {
|
||||
var fileName = fileName ?: return null
|
||||
fileName = fileName.replace('\u202D', '\uFFFD')
|
||||
fileName = fileName.replace('\u202E', '\uFFFD')
|
||||
return fileName
|
||||
}
|
||||
}
|
||||
fun getCleanFileName(fileName: String?): String? =
|
||||
fileName?.replace('\u202D', '\uFFFD')
|
||||
?.replace('\u202E', '\uFFFD')
|
||||
}
|
||||
|
@ -3,8 +3,13 @@ package org.session.libsignal.utilities
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.task
|
||||
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 {
|
||||
return try {
|
||||
get()
|
||||
@ -54,4 +59,11 @@ fun <V> Promise<V, Exception>.timeout(millis: Long): Promise<V, Exception> {
|
||||
if (!deferred.promise.isDone()) { deferred.reject(it) }
|
||||
}
|
||||
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