Merge pull request #315 from vector-im/feature/bma/push2
Push - cleanup 1
This commit is contained in:
commit
d4fdc5b964
119 changed files with 1802 additions and 1037 deletions
|
|
@ -33,7 +33,8 @@ plugins {
|
||||||
id("com.google.firebase.appdistribution") version "4.0.0"
|
id("com.google.firebase.appdistribution") version "4.0.0"
|
||||||
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
id("com.google.gms.google-services")
|
// To be able to update the firebase.xml files, uncomment and build the project
|
||||||
|
// id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -225,9 +226,6 @@ dependencies {
|
||||||
implementation(platform(libs.network.okhttp.bom))
|
implementation(platform(libs.network.okhttp.bom))
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||||
|
|
||||||
implementation(platform(libs.google.firebase.bom))
|
|
||||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
|
||||||
|
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
kapt(libs.dagger.compiler)
|
kapt(libs.dagger.compiler)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "912726360885",
|
|
||||||
"firebase_url": "https://vector-alpha.firebaseio.com",
|
|
||||||
"project_id": "vector-alpha",
|
|
||||||
"storage_bucket": "vector-alpha.appspot.com"
|
|
||||||
},
|
|
||||||
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:912726360885:android:def0a4e454042e9b00427c",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "io.element.android.x.debug"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-hvgoj23p6plt7hikhtdrakihojghaftv.apps.googleusercontent.com",
|
|
||||||
"client_type": 1,
|
|
||||||
"android_info": {
|
|
||||||
"package_name": "io.element.android.x.debug",
|
|
||||||
"certificate_hash": "41bd63b3b612a15d9ba36a5245c393f2a9b992d1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "912726360885",
|
|
||||||
"firebase_url": "https://vector-alpha.firebaseio.com",
|
|
||||||
"project_id": "vector-alpha",
|
|
||||||
"storage_bucket": "vector-alpha.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:912726360885:android:e17435e0beb0303000427c",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "io.element.android.x.nightly"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "912726360885",
|
|
||||||
"firebase_url": "https://vector-alpha.firebaseio.com",
|
|
||||||
"project_id": "vector-alpha",
|
|
||||||
"storage_bucket": "vector-alpha.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:912726360885:android:d097de99a4c23d2700427c",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "io.element.android.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
|
|
@ -45,6 +45,7 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
|
implementation(projects.libraries.pushproviders.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,10 @@ class LoggedInPresenter @Inject constructor(
|
||||||
override fun present(): LoggedInState {
|
override fun present(): LoggedInState {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Ensure pusher is registered
|
// Ensure pusher is registered
|
||||||
pushService.registerFirebasePusher(matrixClient)
|
// TODO Manually select push provider for now
|
||||||
|
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissionsState = postNotificationPermissionsPresenter.present()
|
val permissionsState = postNotificationPermissionsPresenter.present()
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ fun LoggedInView(
|
||||||
state = state.permissionsState,
|
state = state.permissionsState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
openSystemSettings = {
|
openSystemSettings = {
|
||||||
activity?.let { openAppSettingsPage(it, "") }
|
activity?.let { openAppSettingsPage(it) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.libraries.push.providers.api.Distributor
|
||||||
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -55,7 +57,11 @@ class LoggedInPresenterTest {
|
||||||
override fun notificationStyleChanged() {
|
override fun notificationStyleChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun registerFirebasePusher(matrixClient: MatrixClient) {
|
override fun getAvailablePushProviders(): List<PushProvider> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun testPush() {
|
override suspend fun testPush() {
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,6 @@ koverMerged {
|
||||||
target = kotlinx.kover.api.VerificationTarget.CLASS
|
target = kotlinx.kover.api.VerificationTarget.CLASS
|
||||||
overrideClassFilter {
|
overrideClassFilter {
|
||||||
includes += "*Presenter"
|
includes += "*Presenter"
|
||||||
excludes += "*TemplatePresenter"
|
|
||||||
excludes += "*Fake*Presenter"
|
excludes += "*Fake*Presenter"
|
||||||
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class RoomDetailsNode @AssistedInject constructor(
|
class RoomDetailsNode @AssistedInject constructor(
|
||||||
|
|
@ -57,7 +56,6 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
activityResultLauncher = null,
|
activityResultLauncher = null,
|
||||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||||
text = permalink,
|
text = permalink,
|
||||||
noActivityFoundMessage = context.getString(StringR.string.error_no_compatible_app_found)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,8 @@ sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", vers
|
||||||
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3"
|
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3"
|
||||||
sqlite = "androidx.sqlite:sqlite:2.3.1"
|
sqlite = "androidx.sqlite:sqlite:2.3.1"
|
||||||
|
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
||||||
|
gujun_span = "me.gujun.android:span:1.7"
|
||||||
|
|
||||||
# Di
|
# Di
|
||||||
inject = "javax.inject:javax.inject:1"
|
inject = "javax.inject:javax.inject:1"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.ChecksSdkIntAtLeast
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
import io.element.android.libraries.androidutils.R
|
||||||
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
|
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -125,7 +126,7 @@ fun startNotificationSettingsIntent(context: Context, activityResultLauncher: Ac
|
||||||
|
|
||||||
fun openAppSettingsPage(
|
fun openAppSettingsPage(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = activity.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
activity.startActivity(
|
activity.startActivity(
|
||||||
|
|
@ -156,7 +157,7 @@ fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String
|
||||||
fun startAddGoogleAccountIntent(
|
fun startAddGoogleAccountIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
|
val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
|
||||||
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
|
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
|
||||||
|
|
@ -171,7 +172,7 @@ fun startAddGoogleAccountIntent(
|
||||||
fun startInstallFromSourceIntent(
|
fun startInstallFromSourceIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||||
.setData(Uri.parse(String.format("package:%s", context.packageName)))
|
.setData(Uri.parse(String.format("package:%s", context.packageName)))
|
||||||
|
|
@ -189,7 +190,7 @@ fun startSharePlainTextIntent(
|
||||||
text: String,
|
text: String,
|
||||||
subject: String? = null,
|
subject: String? = null,
|
||||||
extraTitle: String? = null,
|
extraTitle: String? = null,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
val share = Intent(Intent.ACTION_SEND)
|
val share = Intent(Intent.ACTION_SEND)
|
||||||
share.type = "text/plain"
|
share.type = "text/plain"
|
||||||
|
|
@ -217,7 +218,7 @@ fun startSharePlainTextIntent(
|
||||||
fun startImportTextFromFileIntent(
|
fun startImportTextFromFileIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
|
|
|
||||||
4
libraries/androidutils/src/main/res/values/localazy.xml
Normal file
4
libraries/androidutils/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="error_no_compatible_app_found">"No compatible app was found to handle this action."</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -16,9 +16,14 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.core
|
package io.element.android.libraries.matrix.api.core
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.BuildConfig
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class EventId(val value: String) : Serializable
|
value class EventId(val value: String) : Serializable
|
||||||
|
|
||||||
fun String.asEventId() = EventId(this)
|
fun String.asEventId() = if (BuildConfig.DEBUG && !MatrixPatterns.isEventId(this)) {
|
||||||
|
error("`$this` is not a valid event Id")
|
||||||
|
} else {
|
||||||
|
EventId(this)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,14 @@ object MatrixPatterns {
|
||||||
PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
|
PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if a string is a valid session Id. This is an alias for [isUserId]
|
||||||
|
*
|
||||||
|
* @param str the string to test
|
||||||
|
* @return true if the string is a valid session id
|
||||||
|
*/
|
||||||
|
fun isSessionId(str: String?) = isUserId(str)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if a string is a valid user Id.
|
* Tells if a string is a valid user Id.
|
||||||
*
|
*
|
||||||
|
|
@ -101,6 +109,14 @@ object MatrixPatterns {
|
||||||
return str != null && str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER
|
return str != null && str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if a string is a valid space id. This is an alias for [isRoomId]
|
||||||
|
*
|
||||||
|
* @param str the string to test
|
||||||
|
* @return true if the string is a valid space Id
|
||||||
|
*/
|
||||||
|
fun isSpaceId(str: String?) = isRoomId(str)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if a string is a valid room id.
|
* Tells if a string is a valid room id.
|
||||||
*
|
*
|
||||||
|
|
@ -134,6 +150,14 @@ object MatrixPatterns {
|
||||||
str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
|
str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if a string is a valid thread id. This is an alias for [isEventId].
|
||||||
|
*
|
||||||
|
* @param str the string to test
|
||||||
|
* @return true if the string is a valid thread id.
|
||||||
|
*/
|
||||||
|
fun isThreadId(str: String?) = isEventId(str)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if a string is a valid group id.
|
* Tells if a string is a valid group id.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,14 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.core
|
package io.element.android.libraries.matrix.api.core
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.BuildConfig
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class RoomId(val value: String) : Serializable
|
value class RoomId(val value: String) : Serializable
|
||||||
|
|
||||||
fun String.asRoomId() = RoomId(this)
|
fun String.asRoomId() = if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) {
|
||||||
|
error("`$this` is not a valid room Id")
|
||||||
|
} else {
|
||||||
|
RoomId(this)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.core
|
package io.element.android.libraries.matrix.api.core
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.BuildConfig
|
||||||
|
|
||||||
typealias SessionId = UserId
|
typealias SessionId = UserId
|
||||||
|
|
||||||
fun String.asSessionId() = SessionId(this)
|
fun String.asSessionId() = if (BuildConfig.DEBUG && !MatrixPatterns.isSessionId(this)) {
|
||||||
|
error("`$this` is not a valid session Id")
|
||||||
|
} else {
|
||||||
|
SessionId(this)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.core
|
package io.element.android.libraries.matrix.api.core
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.BuildConfig
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
|
|
@ -26,4 +27,8 @@ value class SpaceId(val value: String) : Serializable
|
||||||
*/
|
*/
|
||||||
val MAIN_SPACE = SpaceId("!mainSpace")
|
val MAIN_SPACE = SpaceId("!mainSpace")
|
||||||
|
|
||||||
fun String.asSpaceId() = SpaceId(this)
|
fun String.asSpaceId() = if (BuildConfig.DEBUG && !MatrixPatterns.isSpaceId(this)) {
|
||||||
|
error("`$this` is not a valid space Id")
|
||||||
|
} else {
|
||||||
|
SpaceId(this)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,14 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.core
|
package io.element.android.libraries.matrix.api.core
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.BuildConfig
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class ThreadId(val value: String) : Serializable
|
value class ThreadId(val value: String) : Serializable
|
||||||
|
|
||||||
fun String.asThreadId() = ThreadId(this)
|
fun String.asThreadId() = if (BuildConfig.DEBUG && !MatrixPatterns.isThreadId(this)) {
|
||||||
|
error("`$this` is not a valid thread Id")
|
||||||
|
} else {
|
||||||
|
ThreadId(this)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,14 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.core
|
package io.element.android.libraries.matrix.api.core
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.BuildConfig
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class UserId(val value: String) : Serializable
|
value class UserId(val value: String) : Serializable
|
||||||
|
|
||||||
fun String.asUserId() = UserId(this)
|
fun String.asUserId() = if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) {
|
||||||
|
error("`$this` is not a valid user Id")
|
||||||
|
} else {
|
||||||
|
UserId(this)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ package io.element.android.libraries.matrix.api.pusher
|
||||||
|
|
||||||
interface PushersService {
|
interface PushersService {
|
||||||
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
|
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
|
||||||
|
suspend fun unsetHttpPusher(): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,9 @@ class RustPushersService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun unsetHttpPusher(): Result<Unit> {
|
||||||
|
// TODO Missing client API. We need to set the pusher with Kind == null, but we do not have access to this field from the SDK.
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ val A_USER_ID = UserId("@alice:server.org")
|
||||||
val A_USER_ID_2 = UserId("@bob:server.org")
|
val A_USER_ID_2 = UserId("@bob:server.org")
|
||||||
val A_SESSION_ID = SessionId(A_USER_ID.value)
|
val A_SESSION_ID = SessionId(A_USER_ID.value)
|
||||||
val A_SESSION_ID_2 = SessionId(A_USER_ID_2.value)
|
val A_SESSION_ID_2 = SessionId(A_USER_ID_2.value)
|
||||||
val A_SPACE_ID = SpaceId("!aSpaceId")
|
val A_SPACE_ID = SpaceId("!aSpaceId:domain")
|
||||||
val A_ROOM_ID = RoomId("!aRoomId")
|
val A_ROOM_ID = RoomId("!aRoomId:domain")
|
||||||
val A_ROOM_ID_2 = RoomId("!aRoomId2")
|
val A_ROOM_ID_2 = RoomId("!aRoomId2:domain")
|
||||||
val A_THREAD_ID = ThreadId("\$aThreadId")
|
val A_THREAD_ID = ThreadId("\$aThreadId")
|
||||||
val AN_EVENT_ID = EventId("\$anEventId")
|
val AN_EVENT_ID = EventId("\$anEventId")
|
||||||
val AN_EVENT_ID_2 = EventId("\$anEventId2")
|
val AN_EVENT_ID_2 = EventId("\$anEventId2")
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,5 @@ import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
|
||||||
|
|
||||||
class FakePushersService : PushersService {
|
class FakePushersService : PushersService {
|
||||||
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit)
|
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit)
|
||||||
|
override suspend fun unsetHttpPusher(): Result<Unit> = Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -27,4 +28,6 @@ dependencies {
|
||||||
|
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,48 +32,52 @@ fun PermissionsView(
|
||||||
) {
|
) {
|
||||||
if (state.showDialog.not()) return
|
if (state.showDialog.not()) return
|
||||||
|
|
||||||
if (state.permissionGranted) {
|
when {
|
||||||
// Notification Granted, nothing to do
|
state.permissionGranted -> {
|
||||||
} else if (state.permissionAlreadyDenied) {
|
// Notification Granted, nothing to do
|
||||||
// In this case, tell the user to go to the settings
|
}
|
||||||
ConfirmationDialog(
|
state.permissionAlreadyDenied -> {
|
||||||
modifier = modifier,
|
// In this case, tell the user to go to the settings
|
||||||
title = "System",
|
ConfirmationDialog(
|
||||||
content = "In order to let the application display notification, please grant the permission to the system settings",
|
modifier = modifier,
|
||||||
submitText = "Open settings",
|
title = "System",
|
||||||
onSubmitClicked = {
|
content = "In order to let the application display notification, please grant the permission to the system settings",
|
||||||
state.eventSink.invoke(PermissionsEvents.CloseDialog)
|
submitText = "Open settings",
|
||||||
openSystemSettings()
|
onSubmitClicked = {
|
||||||
},
|
state.eventSink.invoke(PermissionsEvents.CloseDialog)
|
||||||
onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) },
|
openSystemSettings()
|
||||||
)
|
},
|
||||||
} else {
|
onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) },
|
||||||
val textToShow = if (state.shouldShowRationale) {
|
)
|
||||||
// TODO Move to state
|
}
|
||||||
// If the user has denied the permission but the rationale can be shown,
|
else -> {
|
||||||
// then gently explain why the app requires this permission
|
val textToShow = if (state.shouldShowRationale) {
|
||||||
// permissions_rationale_msg_notification
|
// TODO Move to state
|
||||||
"To be able to receive notifications, please grant the permission. Else you will not be able to be alerted if you've got new messages."
|
// If the user has denied the permission but the rationale can be shown,
|
||||||
} else {
|
// then gently explain why the app requires this permission
|
||||||
// TODO Move to state
|
// permissions_rationale_msg_notification
|
||||||
// If it's the first time the user lands on this feature, or the user
|
"To be able to receive notifications, please grant the permission. Else you will not be able to be alerted if you've got new messages."
|
||||||
// doesn't want to be asked again for this permission, explain that the
|
} else {
|
||||||
// permission is required
|
// TODO Move to state
|
||||||
"To be able to receive notifications, please grant the permission."
|
// If it's the first time the user lands on this feature, or the user
|
||||||
|
// doesn't want to be asked again for this permission, explain that the
|
||||||
|
// permission is required
|
||||||
|
"To be able to receive notifications, please grant the permission."
|
||||||
|
}
|
||||||
|
ConfirmationDialog(
|
||||||
|
modifier = modifier,
|
||||||
|
title = "Notifications",
|
||||||
|
content = textToShow,
|
||||||
|
submitText = "Request permission",
|
||||||
|
onSubmitClicked = {
|
||||||
|
state.eventSink.invoke(PermissionsEvents.OpenSystemDialog)
|
||||||
|
},
|
||||||
|
onCancelClicked = {
|
||||||
|
state.eventSink.invoke(PermissionsEvents.CloseDialog)
|
||||||
|
},
|
||||||
|
onDismiss = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ConfirmationDialog(
|
|
||||||
modifier = modifier,
|
|
||||||
title = "Notifications",
|
|
||||||
content = textToShow,
|
|
||||||
submitText = "Request permission",
|
|
||||||
onSubmitClicked = {
|
|
||||||
state.eventSink.invoke(PermissionsEvents.OpenSystemDialog)
|
|
||||||
},
|
|
||||||
onCancelClicked = {
|
|
||||||
state.eventSink.invoke(PermissionsEvents.CloseDialog)
|
|
||||||
},
|
|
||||||
onDismiss = {}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ open class PermissionsViewStateProvider : PreviewParameterProvider<PermissionsSt
|
||||||
override val values: Sequence<PermissionsState>
|
override val values: Sequence<PermissionsState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aPermissionsState(),
|
aPermissionsState(),
|
||||||
// Add other state here
|
aPermissionsState().copy(shouldShowRationale = true),
|
||||||
|
aPermissionsState().copy(permissionAlreadyDenied = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
|
|
@ -40,6 +41,8 @@ import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("DefaultPermissionsPresenter")
|
||||||
|
|
||||||
class DefaultPermissionsPresenter @AssistedInject constructor(
|
class DefaultPermissionsPresenter @AssistedInject constructor(
|
||||||
@Assisted val permission: String,
|
@Assisted val permission: String,
|
||||||
private val permissionsStore: PermissionsStore,
|
private val permissionsStore: PermissionsStore,
|
||||||
|
|
@ -71,7 +74,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
||||||
var permissionState: PermissionState? = null
|
var permissionState: PermissionState? = null
|
||||||
|
|
||||||
fun onPermissionResult(result: Boolean) {
|
fun onPermissionResult(result: Boolean) {
|
||||||
Timber.tag("PERMISSION").w("onPermissionResult: $result")
|
Timber.tag(loggerTag.value).d("onPermissionResult: $result")
|
||||||
localCoroutineScope.launch {
|
localCoroutineScope.launch {
|
||||||
permissionsStore.setPermissionAsked(permission, true)
|
permissionsStore.setPermissionAsked(permission, true)
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +82,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// Should show rational true -> denied.
|
// Should show rational true -> denied.
|
||||||
if (permissionState?.status?.shouldShowRationale == true) {
|
if (permissionState?.status?.shouldShowRationale == true) {
|
||||||
Timber.tag("PERMISSION").w("onPermissionResult: reset the store")
|
Timber.tag(loggerTag.value).d("onPermissionResult: setPermissionDenied to true")
|
||||||
localCoroutineScope.launch {
|
localCoroutineScope.launch {
|
||||||
permissionsStore.setPermissionDenied(permission, true)
|
permissionsStore.setPermissionDenied(permission, true)
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +105,6 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
||||||
val showDialog = rememberSaveable { mutableStateOf(permissionState.status !is PermissionStatus.Granted) }
|
val showDialog = rememberSaveable { mutableStateOf(permissionState.status !is PermissionStatus.Granted) }
|
||||||
|
|
||||||
fun handleEvents(event: PermissionsEvents) {
|
fun handleEvents(event: PermissionsEvents) {
|
||||||
Timber.tag("PERMISSION").w("New event: $event")
|
|
||||||
when (event) {
|
when (event) {
|
||||||
PermissionsEvents.CloseDialog -> {
|
PermissionsEvents.CloseDialog -> {
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
|
|
@ -123,7 +125,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
||||||
permissionAlreadyDenied = isAlreadyDenied,
|
permissionAlreadyDenied = isAlreadyDenied,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
).also {
|
).also {
|
||||||
Timber.tag("PERMISSION").w("New state: $it")
|
Timber.tag(loggerTag.value).d("New state: $it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,5 @@ dependencies {
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.pushproviders.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,22 @@
|
||||||
package io.element.android.libraries.push.api
|
package io.element.android.libraries.push.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.push.providers.api.Distributor
|
||||||
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
|
|
||||||
interface PushService {
|
interface PushService {
|
||||||
|
// TODO Move away
|
||||||
fun notificationStyleChanged()
|
fun notificationStyleChanged()
|
||||||
|
|
||||||
// Ensure pusher is registered
|
fun getAvailablePushProviders(): List<PushProvider>
|
||||||
suspend fun registerFirebasePusher(matrixClient: MatrixClient)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will unregister any previous pusher and register a new one with the provided [PushProvider].
|
||||||
|
*
|
||||||
|
* The method has effect only if the [PushProvider] is different than the current one.
|
||||||
|
*/
|
||||||
|
suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor)
|
||||||
|
|
||||||
|
// TODO Move away
|
||||||
suspend fun testPush()
|
suspend fun testPush()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.push.api.model
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Different strategies for Background sync, only applicable to F-Droid version of the app.
|
|
||||||
*/
|
|
||||||
enum class BackgroundSyncMode {
|
|
||||||
/**
|
|
||||||
* In this mode background syncs are scheduled via Workers, meaning that the system will have control on the periodicity
|
|
||||||
* of syncs when battery is low or when the phone is idle (sync will occur in allowed maintenance windows). After completion
|
|
||||||
* the sync work will schedule another one.
|
|
||||||
*/
|
|
||||||
FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mode requires the app to be exempted from battery optimization. Alarms will be launched and will wake up the app
|
|
||||||
* in order to perform the background sync as a foreground service. After completion the service will schedule another alarm
|
|
||||||
*/
|
|
||||||
FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The app won't sync in background.
|
|
||||||
*/
|
|
||||||
FDROID_BACKGROUND_SYNC_MODE_DISABLED;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val DEFAULT_SYNC_DELAY_SECONDS = 60
|
|
||||||
const val DEFAULT_SYNC_TIMEOUT_SECONDS = 6
|
|
||||||
|
|
||||||
fun fromString(value: String?): BackgroundSyncMode = values().firstOrNull { it.name == value }
|
|
||||||
?: FDROID_BACKGROUND_SYNC_MODE_DISABLED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,26 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.api.store
|
package io.element.android.libraries.push.api.store
|
||||||
|
|
||||||
import io.element.android.libraries.push.api.model.BackgroundSyncMode
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface PushDataStore {
|
interface PushDataStore {
|
||||||
val pushCounterFlow: Flow<Int>
|
val pushCounterFlow: Flow<Int>
|
||||||
|
|
||||||
// TODO Move all those settings to the per user store...
|
|
||||||
fun areNotificationEnabledForDevice(): Boolean
|
|
||||||
fun setNotificationEnabledForDevice(enabled: Boolean)
|
|
||||||
|
|
||||||
fun backgroundSyncTimeOut(): Int
|
|
||||||
fun setBackgroundSyncTimeout(timeInSecond: Int)
|
|
||||||
fun backgroundSyncDelay(): Int
|
|
||||||
fun setBackgroundSyncDelay(timeInSecond: Int)
|
|
||||||
fun isBackgroundSyncEnabled(): Boolean
|
|
||||||
fun setFdroidSyncBackgroundMode(mode: BackgroundSyncMode)
|
|
||||||
fun getFdroidSyncBackgroundMode(): BackgroundSyncMode
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if Pin code is disabled, or if user set the settings to see full notification content.
|
|
||||||
*/
|
|
||||||
fun useCompleteNotificationFormat(): Boolean
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,21 +43,20 @@ dependencies {
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.network)
|
implementation(projects.libraries.network)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
api(projects.libraries.pushproviders.api)
|
||||||
|
api(projects.libraries.pushstore.api)
|
||||||
api(projects.libraries.push.api)
|
api(projects.libraries.push.api)
|
||||||
|
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.services.appnavstate.api)
|
implementation(projects.services.appnavstate.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
|
|
||||||
api("me.gujun.android:span:1.7") {
|
api(libs.gujun.span) {
|
||||||
exclude(group = "com.android.support", module = "support-annotations")
|
exclude(group = "com.android.support", module = "support-annotations")
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation(platform(libs.google.firebase.bom))
|
// TODO Temporary use the deprecated LocalBroadcastManager, to be changed later.
|
||||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
||||||
|
|
||||||
// UnifiedPush
|
|
||||||
api("com.github.UnifiedPush:android-connector:2.1.1")
|
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
|
|
|
||||||
|
|
@ -14,63 +14,15 @@
|
||||||
~ limitations under the License.
|
~ limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
|
|
||||||
<!-- Firebase components -->
|
|
||||||
<meta-data
|
|
||||||
android:name="firebase_analytics_collection_deactivated"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".firebase.VectorFirebaseMessagingService"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<!-- UnifiedPush -->
|
|
||||||
<receiver
|
|
||||||
android:name=".unifiedpush.VectorUnifiedPushMessagingReceiver"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"
|
|
||||||
tools:ignore="ExportedReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
|
|
||||||
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
|
|
||||||
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
|
|
||||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
|
|
||||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".unifiedpush.KeepInternalDistributor"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<!--
|
|
||||||
This action is checked to track installed and uninstalled distributors.
|
|
||||||
We declare it to keep the background sync as an internal
|
|
||||||
unifiedpush distributor.
|
|
||||||
-->
|
|
||||||
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".notifications.TestNotificationReceiver"
|
android:name=".notifications.TestNotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".notifications.NotificationBroadcastReceiver"
|
android:name=".notifications.NotificationBroadcastReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -20,27 +20,41 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
import io.element.android.libraries.push.impl.config.PushConfig
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
|
||||||
import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager
|
||||||
import timber.log.Timber
|
import io.element.android.libraries.push.providers.api.Distributor
|
||||||
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
|
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultPushService @Inject constructor(
|
class DefaultPushService @Inject constructor(
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val pushersManager: PushersManager,
|
private val pushersManager: PushersManager,
|
||||||
private val fcmHelper: FcmHelper,
|
private val userPushStoreFactory: UserPushStoreFactory,
|
||||||
|
private val pushProviders: Set<@JvmSuppressWildcards PushProvider>,
|
||||||
) : PushService {
|
) : PushService {
|
||||||
override fun notificationStyleChanged() {
|
override fun notificationStyleChanged() {
|
||||||
notificationDrawerManager.notificationStyleChanged()
|
notificationDrawerManager.notificationStyleChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun registerFirebasePusher(matrixClient: MatrixClient) {
|
override fun getAvailablePushProviders(): List<PushProvider> {
|
||||||
val pushKey = fcmHelper.getFcmToken() ?: return Unit.also {
|
return pushProviders.sortedBy { it.index }
|
||||||
Timber.tag(pushLoggerTag.value).w("Unable to register pusher, Firebase token is not known.")
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current push provider, compare with provided one, then unregister and register if different, and store change.
|
||||||
|
*/
|
||||||
|
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
|
||||||
|
val userPushStore = userPushStoreFactory.create(matrixClient.sessionId)
|
||||||
|
val currentPushProviderName = userPushStore.getPushProviderName()
|
||||||
|
if (currentPushProviderName != pushProvider.name) {
|
||||||
|
// Unregister previous one if any
|
||||||
|
pushProviders.find { it.name == currentPushProviderName }?.unregister(matrixClient)
|
||||||
}
|
}
|
||||||
pushersManager.registerPusher(matrixClient, pushKey, PushConfig.pusher_http_url)
|
pushProvider.registerWith(matrixClient, distributor)
|
||||||
|
// Store new value
|
||||||
|
userPushStore.setPushProviderName(pushProvider.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun testPush() {
|
override suspend fun testPush() {
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl
|
|
||||||
|
|
||||||
interface FcmHelper {
|
|
||||||
fun isFirebaseAvailable(): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the FCM registration token.
|
|
||||||
*
|
|
||||||
* @return the FCM token or null if not received from FCM.
|
|
||||||
*/
|
|
||||||
fun getFcmToken(): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store FCM token to the SharedPrefs.
|
|
||||||
*
|
|
||||||
* @param token the token to store.
|
|
||||||
*/
|
|
||||||
fun storeFcmToken(token: String?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onNewToken may not be called on application upgrade, so ensure my shared pref is set.
|
|
||||||
*
|
|
||||||
* @param pushersManager the instance to register the pusher on.
|
|
||||||
* @param registerPusher whether the pusher should be registered.
|
|
||||||
*/
|
|
||||||
fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean)
|
|
||||||
|
|
||||||
/*
|
|
||||||
fun onEnterForeground(activeSessionHolder: ActiveSessionHolder)
|
|
||||||
|
|
||||||
fun onEnterBackground(activeSessionHolder: ActiveSessionHolder)
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import com.google.android.gms.common.ConnectionResult
|
|
||||||
import com.google.android.gms.common.GoogleApiAvailability
|
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
|
||||||
import io.element.android.libraries.di.DefaultPreferences
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class store the FCM token in SharedPrefs and ensure this token is retrieved.
|
|
||||||
* It has an alter ego in the fdroid variant.
|
|
||||||
*/
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class GoogleFcmHelper @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
@DefaultPreferences private val sharedPrefs: SharedPreferences,
|
|
||||||
) : FcmHelper {
|
|
||||||
override fun isFirebaseAvailable(): Boolean = true
|
|
||||||
|
|
||||||
override fun getFcmToken(): String? {
|
|
||||||
return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun storeFcmToken(token: String?) {
|
|
||||||
sharedPrefs.edit {
|
|
||||||
putString(PREFS_KEY_FCM_TOKEN, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
|
|
||||||
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
|
||||||
if (checkPlayServices(context)) {
|
|
||||||
try {
|
|
||||||
FirebaseMessaging.getInstance().token
|
|
||||||
.addOnSuccessListener { token ->
|
|
||||||
storeFcmToken(token)
|
|
||||||
if (registerPusher) {
|
|
||||||
runBlocking {// TODO
|
|
||||||
pushersManager.enqueueRegisterPusherWithFcmKey(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.addOnFailureListener { e ->
|
|
||||||
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, R.string.push_no_valid_google_play_services_apk_android, Toast.LENGTH_SHORT).show()
|
|
||||||
Timber.e("No valid Google Play Services found. Cannot use FCM.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the device to make sure it has the Google Play Services APK. If
|
|
||||||
* it doesn't, display a dialog that allows users to download the APK from
|
|
||||||
* the Google Play Store or enable it in the device's system settings.
|
|
||||||
*/
|
|
||||||
private fun checkPlayServices(context: Context): Boolean {
|
|
||||||
val apiAvailability = GoogleApiAvailability.getInstance()
|
|
||||||
val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
|
|
||||||
return resultCode == ConnectionResult.SUCCESS
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
override fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) {
|
|
||||||
// No op
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) {
|
|
||||||
// No op
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,20 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl
|
package io.element.android.libraries.push.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
|
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
|
||||||
import io.element.android.libraries.push.impl.clientsecret.PushClientSecret
|
|
||||||
import io.element.android.libraries.push.impl.config.PushConfig
|
import io.element.android.libraries.push.impl.config.PushConfig
|
||||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||||
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
|
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
|
||||||
import io.element.android.libraries.push.impl.userpushstore.UserPushStoreFactory
|
import io.element.android.libraries.push.providers.api.PusherSubscriber
|
||||||
import io.element.android.libraries.push.impl.userpushstore.isFirebase
|
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
import io.element.android.libraries.sessionstorage.api.toUserList
|
|
||||||
import io.element.android.services.toolbox.api.appname.AppNameProvider
|
import io.element.android.services.toolbox.api.appname.AppNameProvider
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -38,62 +37,32 @@ internal const val DEFAULT_PUSHER_FILE_TAG = "mobile"
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("PushersManager", pushLoggerTag)
|
private val loggerTag = LoggerTag("PushersManager", pushLoggerTag)
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
class PushersManager @Inject constructor(
|
class PushersManager @Inject constructor(
|
||||||
private val unifiedPushHelper: UnifiedPushHelper,
|
|
||||||
// private val localeProvider: LocaleProvider,
|
// private val localeProvider: LocaleProvider,
|
||||||
private val appNameProvider: AppNameProvider,
|
private val appNameProvider: AppNameProvider,
|
||||||
// private val getDeviceInfoUseCase: GetDeviceInfoUseCase,
|
// private val getDeviceInfoUseCase: GetDeviceInfoUseCase,
|
||||||
private val pushGatewayNotifyRequest: PushGatewayNotifyRequest,
|
private val pushGatewayNotifyRequest: PushGatewayNotifyRequest,
|
||||||
private val pushClientSecret: PushClientSecret,
|
private val pushClientSecret: PushClientSecret,
|
||||||
private val sessionStore: SessionStore,
|
|
||||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
|
||||||
private val userPushStoreFactory: UserPushStoreFactory,
|
private val userPushStoreFactory: UserPushStoreFactory,
|
||||||
private val fcmHelper: FcmHelper,
|
) : PusherSubscriber {
|
||||||
) {
|
// TODO Move this to the PushProvider API
|
||||||
suspend fun testPush() {
|
suspend fun testPush() {
|
||||||
pushGatewayNotifyRequest.execute(
|
pushGatewayNotifyRequest.execute(
|
||||||
PushGatewayNotifyRequest.Params(
|
PushGatewayNotifyRequest.Params(
|
||||||
url = unifiedPushHelper.getPushGateway() ?: return,
|
url = "TODO", // unifiedPushHelper.getPushGateway() ?: return,
|
||||||
appId = PushConfig.pusher_app_id,
|
appId = PushConfig.pusher_app_id,
|
||||||
pushKey = unifiedPushHelper.getEndpointOrToken().orEmpty(),
|
pushKey = "TODO", // unifiedPushHelper.getEndpointOrToken().orEmpty(),
|
||||||
eventId = TEST_EVENT_ID
|
eventId = TEST_EVENT_ID
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String) {
|
|
||||||
// return onNewFirebaseToken(pushKey, PushConfig.pusher_http_url)
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onNewUnifiedPushEndpoint(
|
|
||||||
pushKey: String,
|
|
||||||
gateway: String
|
|
||||||
) {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onNewFirebaseToken(firebaseToken: String) {
|
|
||||||
fcmHelper.storeFcmToken(firebaseToken)
|
|
||||||
|
|
||||||
// Register the pusher for all the sessions
|
|
||||||
sessionStore.getAllSessions().toUserList().forEach { userId ->
|
|
||||||
val userDataStore = userPushStoreFactory.create(userId)
|
|
||||||
if (userDataStore.isFirebase()) {
|
|
||||||
matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client ->
|
|
||||||
registerPusher(client, firebaseToken, PushConfig.pusher_http_url)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).d("This session is not using Firebase pusher")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a pusher to the server if not done yet.
|
* Register a pusher to the server if not done yet.
|
||||||
*/
|
*/
|
||||||
suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
|
override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
|
||||||
val userDataStore = userPushStoreFactory.create(matrixClient.sessionId.value)
|
val userDataStore = userPushStoreFactory.create(matrixClient.sessionId)
|
||||||
if (userDataStore.getCurrentRegisteredPushKey() == pushKey) {
|
if (userDataStore.getCurrentRegisteredPushKey() == pushKey) {
|
||||||
Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher")
|
Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -162,9 +131,8 @@ class PushersManager @Inject constructor(
|
||||||
// currentSession.pushersService().removeEmailPusher(email)
|
// currentSession.pushersService().removeEmailPusher(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun unregisterPusher(pushKey: String) {
|
override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
|
||||||
// val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
|
matrixClient.pushersService().unsetHttpPusher()
|
||||||
// currentSession.pushersService().removeHttpPusher(pushKey, PushConfig.pusher_app_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import io.element.android.libraries.androidutils.system.getApplicationLabel
|
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
|
||||||
import io.element.android.libraries.push.impl.config.PushConfig
|
|
||||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.unifiedpush.android.connector.UnifiedPush
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.net.URL
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class UnifiedPushHelper @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val unifiedPushStore: UnifiedPushStore,
|
|
||||||
// private val matrix: Matrix,
|
|
||||||
private val fcmHelper: FcmHelper,
|
|
||||||
private val stringProvider: StringProvider,
|
|
||||||
) {
|
|
||||||
|
|
||||||
/* TODO EAx
|
|
||||||
@MainThread
|
|
||||||
fun showSelectDistributorDialog(
|
|
||||||
context: Context,
|
|
||||||
onDistributorSelected: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
val internalDistributorName = stringProvider.getString(
|
|
||||||
if (fcmHelper.isFirebaseAvailable()) {
|
|
||||||
R.string.push_distributor_firebase_android
|
|
||||||
} else {
|
|
||||||
R.string.push_distributor_background_sync_android
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val distributors = UnifiedPush.getDistributors(context)
|
|
||||||
val distributorsName = distributors.map {
|
|
||||||
if (it == context.packageName) {
|
|
||||||
internalDistributorName
|
|
||||||
} else {
|
|
||||||
context.getApplicationLabel(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(context)
|
|
||||||
.setTitle(stringProvider.getString(R.string.push_choose_distributor_dialog_title_android))
|
|
||||||
.setItems(distributorsName.toTypedArray()) { _, which ->
|
|
||||||
val distributor = distributors[which]
|
|
||||||
onDistributorSelected(distributor)
|
|
||||||
}
|
|
||||||
.setOnCancelListener {
|
|
||||||
// we do not want to change the distributor on behalf of the user
|
|
||||||
if (UnifiedPush.getDistributor(context).isEmpty()) {
|
|
||||||
// By default, use internal solution (fcm/background sync)
|
|
||||||
onDistributorSelected(context.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setCancelable(true)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
internal data class DiscoveryResponse(
|
|
||||||
@SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
internal data class DiscoveryUnifiedPush(
|
|
||||||
@SerialName("gateway") val gateway: String = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun storeCustomOrDefaultGateway(
|
|
||||||
endpoint: String,
|
|
||||||
onDoneRunnable: Runnable? = null
|
|
||||||
) {
|
|
||||||
// if we use the embedded distributor,
|
|
||||||
// register app_id type upfcm on sygnal
|
|
||||||
// the pushkey if FCM key
|
|
||||||
if (UnifiedPush.getDistributor(context) == context.packageName) {
|
|
||||||
unifiedPushStore.storePushGateway(PushConfig.pusher_http_url)
|
|
||||||
onDoneRunnable?.run()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
/* TODO EAx UnifiedPush
|
|
||||||
// else, unifiedpush, and pushkey is an endpoint
|
|
||||||
val gateway = PushConfig.default_push_gateway_http_url
|
|
||||||
val parsed = URL(endpoint)
|
|
||||||
val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify"
|
|
||||||
Timber.i("Testing $custom")
|
|
||||||
try {
|
|
||||||
val response = matrix.rawService().getUrl(custom, CacheStrategy.NoCache)
|
|
||||||
tryOrNull { Json.decodeFromString<DiscoveryResponse>(response) }
|
|
||||||
?.let { discoveryResponse ->
|
|
||||||
if (discoveryResponse.unifiedpush.gateway == "matrix") {
|
|
||||||
Timber.d("Using custom gateway")
|
|
||||||
unifiedPushStore.storePushGateway(custom)
|
|
||||||
onDoneRunnable?.run()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.d(e, "Cannot try custom gateway")
|
|
||||||
}
|
|
||||||
unifiedPushStore.storePushGateway(gateway)
|
|
||||||
onDoneRunnable?.run()
|
|
||||||
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getExternalDistributors(): List<String> {
|
|
||||||
return UnifiedPush.getDistributors(context)
|
|
||||||
.filterNot { it == context.packageName }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentDistributorName(): String {
|
|
||||||
return when {
|
|
||||||
isEmbeddedDistributor() -> stringProvider.getString(R.string.push_distributor_firebase_android)
|
|
||||||
isBackgroundSync() -> stringProvider.getString(R.string.push_distributor_background_sync_android)
|
|
||||||
else -> context.getApplicationLabel(UnifiedPush.getDistributor(context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmbeddedDistributor(): Boolean {
|
|
||||||
return isInternalDistributor() && fcmHelper.isFirebaseAvailable()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isBackgroundSync(): Boolean {
|
|
||||||
return isInternalDistributor() && !fcmHelper.isFirebaseAvailable()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isInternalDistributor(): Boolean {
|
|
||||||
return UnifiedPush.getDistributor(context).isEmpty() ||
|
|
||||||
UnifiedPush.getDistributor(context) == context.packageName
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPrivacyFriendlyUpEndpoint(): String? {
|
|
||||||
val endpoint = getEndpointOrToken()
|
|
||||||
if (endpoint.isNullOrEmpty()) return null
|
|
||||||
if (isEmbeddedDistributor()) {
|
|
||||||
return endpoint
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
val parsed = URL(endpoint)
|
|
||||||
"${parsed.protocol}://${parsed.host}/***"
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error parsing unifiedpush endpoint")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getEndpointOrToken(): String? {
|
|
||||||
return if (isEmbeddedDistributor()) fcmHelper.getFcmToken()
|
|
||||||
else unifiedPushStore.getEndpoint()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPushGateway(): String? {
|
|
||||||
return if (isEmbeddedDistributor()) PushConfig.pusher_http_url
|
|
||||||
else unifiedPushStore.getPushGateway()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -17,25 +17,8 @@
|
||||||
package io.element.android.libraries.push.impl.config
|
package io.element.android.libraries.push.impl.config
|
||||||
|
|
||||||
object PushConfig {
|
object PushConfig {
|
||||||
/**
|
|
||||||
* It is the push gateway for FCM embedded distributor.
|
|
||||||
* Note: pusher_http_url should have path '/_matrix/push/v1/notify' -->
|
|
||||||
*/
|
|
||||||
const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It is the push gateway for UnifiedPush.
|
|
||||||
* Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify'
|
|
||||||
*/
|
|
||||||
const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: pusher_app_id cannot exceed 64 chars.
|
* Note: pusher_app_id cannot exceed 64 chars.
|
||||||
*/
|
*/
|
||||||
const val pusher_app_id: String = "im.vector.app.android"
|
const val pusher_app_id: String = "im.vector.app.android"
|
||||||
|
|
||||||
/**
|
|
||||||
* Set to true to allow external push distributor such as Ntfy.
|
|
||||||
*/
|
|
||||||
const val allowExternalUnifiedPushDistributors: Boolean = false
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ class NotificationDrawerManager @Inject constructor(
|
||||||
private var currentAppNavigationState: AppNavigationState? = null
|
private var currentAppNavigationState: AppNavigationState? = null
|
||||||
private val firstThrottler = FirstThrottler(200)
|
private val firstThrottler = FirstThrottler(200)
|
||||||
|
|
||||||
private var useCompleteNotificationFormat = pushDataStore.useCompleteNotificationFormat()
|
// TODO EAx add a setting per user for this
|
||||||
|
private var useCompleteNotificationFormat = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
handlerThread.start()
|
handlerThread.start()
|
||||||
|
|
@ -111,12 +112,6 @@ class NotificationDrawerManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
private fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||||
if (!pushDataStore.areNotificationEnabledForDevice()) {
|
|
||||||
Timber.i("Notification are disabled for this device")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If we support multi session, event list should be per userId
|
|
||||||
// Currently only manage single session
|
|
||||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||||
Timber.d("onNotifiableEventReceived(): $notifiableEvent")
|
Timber.d("onNotifiableEventReceived(): $notifiableEvent")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -185,7 +180,7 @@ class NotificationDrawerManager @Inject constructor(
|
||||||
// TODO EAx Must be per account
|
// TODO EAx Must be per account
|
||||||
fun notificationStyleChanged() {
|
fun notificationStyleChanged() {
|
||||||
updateEvents {
|
updateEvents {
|
||||||
val newSettings = pushDataStore.useCompleteNotificationFormat()
|
val newSettings = true // pushDataStore.useCompleteNotificationFormat()
|
||||||
if (newSettings != useCompleteNotificationFormat) {
|
if (newSettings != useCompleteNotificationFormat) {
|
||||||
// Settings has changed, remove all current notifications
|
// Settings has changed, remove all current notifications
|
||||||
notificationRenderer.cancelAllNotifications()
|
notificationRenderer.cancelAllNotifications()
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ data class NotificationEventQueue constructor(
|
||||||
* Acts as a notification debouncer to stop already dismissed push notifications from
|
* Acts as a notification debouncer to stop already dismissed push notifications from
|
||||||
* displaying again when the /sync response is delayed.
|
* displaying again when the /sync response is delayed.
|
||||||
*/
|
*/
|
||||||
|
// TODO Should be per session, so the key must be Pair<SessionId, EventId>.
|
||||||
private val seenEventIds: CircularCache<EventId>
|
private val seenEventIds: CircularCache<EventId>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,19 +21,22 @@ import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import io.element.android.libraries.androidutils.network.WifiDetector
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import io.element.android.libraries.push.api.store.PushDataStore
|
|
||||||
import io.element.android.libraries.push.impl.PushersManager
|
import io.element.android.libraries.push.impl.PushersManager
|
||||||
import io.element.android.libraries.push.impl.clientsecret.PushClientSecret
|
|
||||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
|
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
|
||||||
import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager
|
||||||
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
|
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
|
||||||
|
import io.element.android.libraries.push.providers.api.PushData
|
||||||
|
import io.element.android.libraries.push.providers.api.PushHandler
|
||||||
|
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||||
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
|
@ -43,20 +46,20 @@ import javax.inject.Inject
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("PushHandler", pushLoggerTag)
|
private val loggerTag = LoggerTag("PushHandler", pushLoggerTag)
|
||||||
|
|
||||||
class PushHandler @Inject constructor(
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultPushHandler @Inject constructor(
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val notifiableEventResolver: NotifiableEventResolver,
|
private val notifiableEventResolver: NotifiableEventResolver,
|
||||||
private val pushDataStore: PushDataStore,
|
|
||||||
private val defaultPushDataStore: DefaultPushDataStore,
|
private val defaultPushDataStore: DefaultPushDataStore,
|
||||||
|
private val userPushStoreFactory: UserPushStoreFactory,
|
||||||
private val pushClientSecret: PushClientSecret,
|
private val pushClientSecret: PushClientSecret,
|
||||||
private val actionIds: NotificationActionIds,
|
private val actionIds: NotificationActionIds,
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||||
) {
|
) : PushHandler {
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob())
|
private val coroutineScope = CoroutineScope(SupervisorJob())
|
||||||
private val wifiDetector: WifiDetector = WifiDetector(context)
|
|
||||||
|
|
||||||
// UI handler
|
// UI handler
|
||||||
private val mUIHandler by lazy {
|
private val mUIHandler by lazy {
|
||||||
|
|
@ -68,7 +71,7 @@ class PushHandler @Inject constructor(
|
||||||
*
|
*
|
||||||
* @param pushData the data received in the push.
|
* @param pushData the data received in the push.
|
||||||
*/
|
*/
|
||||||
suspend fun handle(pushData: PushData) {
|
override suspend fun handle(pushData: PushData) {
|
||||||
Timber.tag(loggerTag.value).d("## handling pushData")
|
Timber.tag(loggerTag.value).d("## handling pushData")
|
||||||
|
|
||||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||||
|
|
@ -84,12 +87,6 @@ class PushHandler @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO EAx Should be per user
|
|
||||||
if (!pushDataStore.areNotificationEnabledForDevice()) {
|
|
||||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mUIHandler.post {
|
mUIHandler.post {
|
||||||
coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) }
|
coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) }
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +131,13 @@ class PushHandler @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val userPushStore = userPushStoreFactory.create(userId)
|
||||||
|
if (!userPushStore.areNotificationEnabledForDevice()) {
|
||||||
|
// TODO We need to check if this is an incoming call
|
||||||
|
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
notificationDrawerManager.onNotifiableEventReceived(notificationData)
|
notificationDrawerManager.onNotifiableEventReceived(notificationData)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||||
|
|
@ -17,20 +17,15 @@
|
||||||
package io.element.android.libraries.push.impl.store
|
package io.element.android.libraries.push.impl.store
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.DefaultPreferences
|
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.push.api.model.BackgroundSyncMode
|
|
||||||
import io.element.android.libraries.push.api.store.PushDataStore
|
import io.element.android.libraries.push.api.store.PushDataStore
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -42,7 +37,6 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultPushDataStore @Inject constructor(
|
class DefaultPushDataStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
@DefaultPreferences private val defaultPrefs: SharedPreferences,
|
|
||||||
) : PushDataStore {
|
) : PushDataStore {
|
||||||
private val pushCounter = intPreferencesKey("push_counter")
|
private val pushCounter = intPreferencesKey("push_counter")
|
||||||
|
|
||||||
|
|
@ -56,94 +50,4 @@ class DefaultPushDataStore @Inject constructor(
|
||||||
settings[pushCounter] = currentCounterValue + 1
|
settings[pushCounter] = currentCounterValue + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areNotificationEnabledForDevice(): Boolean {
|
|
||||||
return defaultPrefs.getBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setNotificationEnabledForDevice(enabled: Boolean) {
|
|
||||||
defaultPrefs.edit {
|
|
||||||
putBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun backgroundSyncTimeOut(): Int {
|
|
||||||
return tryOrNull {
|
|
||||||
// The xml pref is saved as a string so use getString and parse
|
|
||||||
defaultPrefs.getString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, null)?.toInt()
|
|
||||||
} ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setBackgroundSyncTimeout(timeInSecond: Int) {
|
|
||||||
defaultPrefs
|
|
||||||
.edit()
|
|
||||||
.putString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, timeInSecond.toString())
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun backgroundSyncDelay(): Int {
|
|
||||||
return tryOrNull {
|
|
||||||
// The xml pref is saved as a string so use getString and parse
|
|
||||||
defaultPrefs.getString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, null)?.toInt()
|
|
||||||
} ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setBackgroundSyncDelay(timeInSecond: Int) {
|
|
||||||
defaultPrefs
|
|
||||||
.edit()
|
|
||||||
.putString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, timeInSecond.toString())
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isBackgroundSyncEnabled(): Boolean {
|
|
||||||
return getFdroidSyncBackgroundMode() != BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setFdroidSyncBackgroundMode(mode: BackgroundSyncMode) {
|
|
||||||
defaultPrefs
|
|
||||||
.edit()
|
|
||||||
.putString(SETTINGS_FDROID_BACKGROUND_SYNC_MODE, mode.name)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFdroidSyncBackgroundMode(): BackgroundSyncMode {
|
|
||||||
return try {
|
|
||||||
val strPref = defaultPrefs
|
|
||||||
.getString(SETTINGS_FDROID_BACKGROUND_SYNC_MODE, BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY.name)
|
|
||||||
BackgroundSyncMode.values().firstOrNull { it.name == strPref } ?: BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if Pin code is disabled, or if user set the settings to see full notification content.
|
|
||||||
*/
|
|
||||||
override fun useCompleteNotificationFormat(): Boolean {
|
|
||||||
return true
|
|
||||||
/*
|
|
||||||
return !useFlagPinCode() ||
|
|
||||||
defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG, true)
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// notifications
|
|
||||||
const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"
|
|
||||||
const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY"
|
|
||||||
|
|
||||||
// background sync
|
|
||||||
const val SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY"
|
|
||||||
const val SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY"
|
|
||||||
const val SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY"
|
|
||||||
const val SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY"
|
|
||||||
|
|
||||||
const val SETTINGS_FDROID_BACKGROUND_SYNC_MODE = "SETTINGS_FDROID_BACKGROUND_SYNC_MODE"
|
|
||||||
const val SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY"
|
|
||||||
|
|
||||||
const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG"
|
|
||||||
|
|
||||||
// notification method
|
|
||||||
const val SETTINGS_NOTIFICATION_METHOD_KEY = "SETTINGS_NOTIFICATION_METHOD_KEY"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
libraries/pushproviders/api/build.gradle.kts
Normal file
28
libraries/pushproviders/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.push.providers.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.api
|
||||||
|
|
||||||
|
data class Distributor(
|
||||||
|
val value: String,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.push
|
package io.element.android.libraries.push.providers.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -22,14 +22,14 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
/**
|
/**
|
||||||
* Represent parsed data that the app has received from a Push content.
|
* Represent parsed data that the app has received from a Push content.
|
||||||
*
|
*
|
||||||
* @property eventId The Event ID. If not null, it will not be empty, and will have a valid format.
|
* @property eventId The Event Id.
|
||||||
* @property roomId The Room ID. If not null, it will not be empty, and will have a valid format.
|
* @property roomId The Room Id.
|
||||||
* @property unread Number of unread message.
|
* @property unread Number of unread message.
|
||||||
* @property clientSecret A client secret, used to determine which user should receive the notification.
|
* @property clientSecret data used when the pusher was configured, to be able to determine the session.
|
||||||
*/
|
*/
|
||||||
data class PushData(
|
data class PushData(
|
||||||
val eventId: EventId?,
|
val eventId: EventId,
|
||||||
val roomId: RoomId?,
|
val roomId: RoomId,
|
||||||
val unread: Int?,
|
val unread: Int?,
|
||||||
val clientSecret: String?,
|
val clientSecret: String?,
|
||||||
)
|
)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.api
|
||||||
|
|
||||||
|
interface PushHandler {
|
||||||
|
suspend fun handle(pushData: PushData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the main API for this module.
|
||||||
|
*/
|
||||||
|
interface PushProvider {
|
||||||
|
/**
|
||||||
|
* Allow to sort providers, from lower index to higher index.
|
||||||
|
*/
|
||||||
|
val index: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User friendly name.
|
||||||
|
*/
|
||||||
|
val name: String
|
||||||
|
|
||||||
|
fun getDistributors(): List<Distributor>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the pusher to the homeserver.
|
||||||
|
*/
|
||||||
|
suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister the pusher.
|
||||||
|
*/
|
||||||
|
suspend fun unregister(matrixClient: MatrixClient)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to troubleshoot the push provider.
|
||||||
|
*/
|
||||||
|
suspend fun troubleshoot(): Result<Unit>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
||||||
|
interface PusherSubscriber {
|
||||||
|
suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String)
|
||||||
|
suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String)
|
||||||
|
}
|
||||||
7
libraries/pushproviders/firebase/README.md
Normal file
7
libraries/pushproviders/firebase/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Firebase
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
In order to make this module only know about Firebase, the plugin `com.google.gms.google-services` has been disabled from the `app` module.
|
||||||
|
|
||||||
|
To be able to change the values in the file `firebase.xml` from this module, you should enable the plugin `com.google.gms.google-services` again, copy the file `google-services.json` to the folder `/app/src/main`, build the project, and check the generated file `app/build/generated/res/google-services/<buildtype>/values/values.xml` to import the generated values into the `firebase.xml` files.
|
||||||
48
libraries/pushproviders/firebase/build.gradle.kts
Normal file
48
libraries/pushproviders/firebase/build.gradle.kts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.push.providers.firebase"
|
||||||
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.dagger)
|
||||||
|
implementation(libs.androidx.corektx)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.di)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
||||||
|
implementation(projects.libraries.pushstore.api)
|
||||||
|
implementation(projects.libraries.pushproviders.api)
|
||||||
|
|
||||||
|
api(platform(libs.google.firebase.bom))
|
||||||
|
api("com.google.firebase:firebase-messaging-ktx")
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="google_app_id" translatable="false">1:912726360885:android:def0a4e454042e9b00427c</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2023 New Vector Ltd
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application>
|
||||||
|
<!-- Firebase components -->
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_analytics_collection_deactivated"
|
||||||
|
android:value="true" />
|
||||||
|
<service
|
||||||
|
android:name="io.element.android.libraries.push.providers.firebase.VectorFirebaseMessagingService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
|
@ -14,24 +14,22 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.firebase
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
import io.element.android.libraries.push.impl.FcmHelper
|
|
||||||
import io.element.android.libraries.push.impl.PushersManager
|
|
||||||
import io.element.android.libraries.push.impl.UnifiedPushHelper
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// TODO
|
||||||
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
|
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
|
||||||
private val unifiedPushHelper: UnifiedPushHelper,
|
// private val unifiedPushHelper: UnifiedPushHelper,
|
||||||
private val fcmHelper: FcmHelper,
|
// private val fcmHelper: FcmHelper,
|
||||||
// private val activeSessionHolder: ActiveSessionHolder,
|
// private val activeSessionHolder: ActiveSessionHolder,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
|
// fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
|
||||||
if (unifiedPushHelper.isEmbeddedDistributor()) {
|
// if (unifiedPushHelper.isEmbeddedDistributor()) {
|
||||||
fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
|
// fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
|
private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
|
||||||
/*
|
/*
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
|
object FirebaseConfig {
|
||||||
|
/**
|
||||||
|
* It is the push gateway for firebase.
|
||||||
|
* Note: pusher_http_url should have path '/_matrix/push/v1/notify' -->
|
||||||
|
*/
|
||||||
|
const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify"
|
||||||
|
|
||||||
|
const val index = 0
|
||||||
|
const val name = "Firebase"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.matrix.api.core.asSessionId
|
||||||
|
import io.element.android.libraries.push.providers.api.PusherSubscriber
|
||||||
|
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.api.toUserList
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("FirebaseNewTokenHandler")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider.
|
||||||
|
*/
|
||||||
|
class FirebaseNewTokenHandler @Inject constructor(
|
||||||
|
private val pusherSubscriber: PusherSubscriber,
|
||||||
|
private val sessionStore: SessionStore,
|
||||||
|
private val userPushStoreFactory: UserPushStoreFactory,
|
||||||
|
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||||
|
private val firebaseStore: FirebaseStore,
|
||||||
|
) {
|
||||||
|
suspend fun handle(firebaseToken: String) {
|
||||||
|
firebaseStore.storeFcmToken(firebaseToken)
|
||||||
|
// Register the pusher for all the sessions
|
||||||
|
sessionStore.getAllSessions().toUserList()
|
||||||
|
.mapNotNull { it.asSessionId() }
|
||||||
|
.forEach { userId ->
|
||||||
|
val userDataStore = userPushStoreFactory.create(userId)
|
||||||
|
if (userDataStore.getPushProviderName() == FirebaseConfig.name) {
|
||||||
|
matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client ->
|
||||||
|
pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.tag(loggerTag.value).d("This session is not using Firebase pusher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,18 +14,17 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.firebase
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.push.providers.api.PushData
|
||||||
import io.element.android.libraries.push.impl.push.PushData
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FirebasePushParser @Inject constructor() {
|
class FirebasePushParser @Inject constructor() {
|
||||||
fun parse(message: Map<String, String?>): PushData {
|
fun parse(message: Map<String, String?>): PushData? {
|
||||||
val pushDataFirebase = PushDataFirebase(
|
val pushDataFirebase = PushDataFirebase(
|
||||||
eventId = message["event_id"],
|
eventId = message["event_id"],
|
||||||
roomId = message["room_id"],
|
roomId = message["room_id"],
|
||||||
unread = message["unread"]?.let { tryOrNull { Integer.parseInt(it) } },
|
unread = message["unread"]?.toIntOrNull(),
|
||||||
clientSecret = message["cs"],
|
clientSecret = message["cs"],
|
||||||
)
|
)
|
||||||
return pushDataFirebase.toPushData()
|
return pushDataFirebase.toPushData()
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.push.providers.api.Distributor
|
||||||
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
|
import io.element.android.libraries.push.providers.api.PusherSubscriber
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("FirebasePushProvider")
|
||||||
|
|
||||||
|
class FirebasePushProvider @Inject constructor(
|
||||||
|
private val firebaseStore: FirebaseStore,
|
||||||
|
private val firebaseTroubleshooter: FirebaseTroubleshooter,
|
||||||
|
private val pusherSubscriber: PusherSubscriber,
|
||||||
|
) : PushProvider {
|
||||||
|
override val index = FirebaseConfig.index
|
||||||
|
override val name = FirebaseConfig.name
|
||||||
|
|
||||||
|
override fun getDistributors(): List<Distributor> {
|
||||||
|
return listOf(Distributor("Firebase", "Firebase"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) {
|
||||||
|
val pushKey = firebaseStore.getFcmToken() ?: return Unit.also {
|
||||||
|
Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.")
|
||||||
|
}
|
||||||
|
pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unregister(matrixClient: MatrixClient) {
|
||||||
|
val pushKey = firebaseStore.getFcmToken() ?: return Unit.also {
|
||||||
|
Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.")
|
||||||
|
}
|
||||||
|
pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun troubleshoot(): Result<Unit> {
|
||||||
|
return firebaseTroubleshooter.troubleshoot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import io.element.android.libraries.di.DefaultPreferences
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class store the Firebase token in SharedPrefs.
|
||||||
|
*/
|
||||||
|
class FirebaseStore @Inject constructor(
|
||||||
|
@DefaultPreferences private val sharedPrefs: SharedPreferences,
|
||||||
|
) {
|
||||||
|
fun getFcmToken(): String? {
|
||||||
|
return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun storeFcmToken(token: String?) {
|
||||||
|
sharedPrefs.edit {
|
||||||
|
putString(PREFS_KEY_FCM_TOKEN, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.android.gms.common.ConnectionResult
|
||||||
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class force retrieving and storage of the Firebase token.
|
||||||
|
*/
|
||||||
|
class FirebaseTroubleshooter @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val newTokenHandler: FirebaseNewTokenHandler,
|
||||||
|
) {
|
||||||
|
suspend fun troubleshoot(): Result<Unit> {
|
||||||
|
return runCatching {
|
||||||
|
val token = retrievedFirebaseToken()
|
||||||
|
newTokenHandler.handle(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun retrievedFirebaseToken(): String {
|
||||||
|
return suspendCoroutine { continuation ->
|
||||||
|
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||||
|
if (checkPlayServices(context)) {
|
||||||
|
try {
|
||||||
|
FirebaseMessaging.getInstance().token
|
||||||
|
.addOnSuccessListener { token ->
|
||||||
|
continuation.resume(token)
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Timber.e(e, "## retrievedFirebaseToken() : failed")
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Timber.e(e, "## retrievedFirebaseToken() : failed")
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val e = Exception("No valid Google Play Services found. Cannot use FCM.")
|
||||||
|
Timber.e(e)
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the device to make sure it has the Google Play Services APK. If
|
||||||
|
* it doesn't, display a dialog that allows users to download the APK from
|
||||||
|
* the Google Play Store or enable it in the device's system settings.
|
||||||
|
*/
|
||||||
|
private fun checkPlayServices(context: Context): Boolean {
|
||||||
|
val apiAvailability = GoogleApiAvailability.getInstance()
|
||||||
|
val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
|
||||||
|
return resultCode == ConnectionResult.SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,12 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.firebase
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
|
||||||
import io.element.android.libraries.matrix.api.core.asEventId
|
import io.element.android.libraries.matrix.api.core.asEventId
|
||||||
import io.element.android.libraries.matrix.api.core.asRoomId
|
import io.element.android.libraries.matrix.api.core.asRoomId
|
||||||
import io.element.android.libraries.push.impl.push.PushData
|
import io.element.android.libraries.push.providers.api.PushData
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In this case, the format is:
|
* In this case, the format is:
|
||||||
|
|
@ -41,9 +40,13 @@ data class PushDataFirebase(
|
||||||
val clientSecret: String?
|
val clientSecret: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
fun PushDataFirebase.toPushData() = PushData(
|
fun PushDataFirebase.toPushData(): PushData? {
|
||||||
eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(),
|
val safeEventId = eventId?.asEventId() ?: return null
|
||||||
roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(),
|
val safeRoomId = roomId?.asRoomId() ?: return null
|
||||||
unread = unread,
|
return PushData(
|
||||||
clientSecret = clientSecret,
|
eventId = safeEventId,
|
||||||
)
|
roomId = safeRoomId,
|
||||||
|
unread = unread,
|
||||||
|
clientSecret = clientSecret,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -14,25 +14,23 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.firebase
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.push.impl.PushersManager
|
import io.element.android.libraries.push.providers.api.PushHandler
|
||||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
|
||||||
import io.element.android.libraries.push.impl.push.PushHandler
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("Firebase", pushLoggerTag)
|
private val loggerTag = LoggerTag("Firebase")
|
||||||
|
|
||||||
class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
@Inject lateinit var pushersManager: PushersManager
|
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
|
||||||
@Inject lateinit var pushParser: FirebasePushParser
|
@Inject lateinit var pushParser: FirebasePushParser
|
||||||
@Inject lateinit var pushHandler: PushHandler
|
@Inject lateinit var pushHandler: PushHandler
|
||||||
|
|
||||||
|
|
@ -46,15 +44,18 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
Timber.tag(loggerTag.value).d("New Firebase token")
|
Timber.tag(loggerTag.value).d("New Firebase token")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
pushersManager.onNewFirebaseToken(token)
|
firebaseNewTokenHandler.handle(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMessageReceived(message: RemoteMessage) {
|
override fun onMessageReceived(message: RemoteMessage) {
|
||||||
Timber.tag(loggerTag.value).d("New Firebase message")
|
Timber.tag(loggerTag.value).d("New Firebase message")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
pushParser.parse(message.data).let {
|
val pushData = pushParser.parse(message.data)
|
||||||
pushHandler.handle(it)
|
if (pushData == null) {
|
||||||
|
Timber.tag(loggerTag.value).w("Invalid data received from Firebase")
|
||||||
|
} else {
|
||||||
|
pushHandler.handle(pushData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.firebase
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.firebase.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.multibindings.IntoSet
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
|
import io.element.android.libraries.push.providers.firebase.FirebasePushProvider
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(AppScope::class)
|
||||||
|
interface FirebaseModule {
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
fun bind(pushProvider: FirebasePushProvider): PushProvider
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="default_web_client_id" translatable="false">912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com</string>
|
||||||
|
<string name="firebase_database_url" translatable="false">https://vector-alpha.firebaseio.com</string>
|
||||||
|
<string name="gcm_defaultSenderId" translatable="false">912726360885</string>
|
||||||
|
<string name="google_api_key" translatable="false">AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c</string>
|
||||||
|
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c</string>
|
||||||
|
<string name="google_storage_bucket" translatable="false">vector-alpha.appspot.com</string>
|
||||||
|
<string name="project_id" translatable="false">vector-alpha</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="google_app_id" translatable="false">1:912726360885:android:e17435e0beb0303000427c</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="google_app_id" translatable="false">1:912726360885:android:d097de99a4c23d2700427c</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.firebase
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.push.providers.api.PushData
|
||||||
|
import io.element.android.tests.testutils.assertNullOrThrow
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class FirebasePushParserTest {
|
||||||
|
private val validData = PushData(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
unread = 1,
|
||||||
|
clientSecret = "a-secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test edge cases Firebase`() {
|
||||||
|
val pushParser = FirebasePushParser()
|
||||||
|
// Empty Json
|
||||||
|
assertThat(pushParser.parse(emptyMap())).isNull()
|
||||||
|
// Bad Json
|
||||||
|
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("unread", "str"))).isEqualTo(validData.copy(unread = null))
|
||||||
|
// Extra data
|
||||||
|
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("extra", "5"))).isEqualTo(validData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test Firebase format`() {
|
||||||
|
val pushParser = FirebasePushParser()
|
||||||
|
assertThat(pushParser.parse(FIREBASE_PUSH_DATA)).isEqualTo(validData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test empty roomId`() {
|
||||||
|
val pushParser = FirebasePushParser()
|
||||||
|
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull()
|
||||||
|
assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test invalid roomId`() {
|
||||||
|
val pushParser = FirebasePushParser()
|
||||||
|
assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test empty eventId`() {
|
||||||
|
val pushParser = FirebasePushParser()
|
||||||
|
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isNull()
|
||||||
|
assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test invalid eventId`() {
|
||||||
|
val pushParser = FirebasePushParser()
|
||||||
|
assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val FIREBASE_PUSH_DATA = mapOf(
|
||||||
|
"event_id" to AN_EVENT_ID.value,
|
||||||
|
"room_id" to A_ROOM_ID.value,
|
||||||
|
"unread" to "1",
|
||||||
|
"prio" to "high",
|
||||||
|
"cs" to "a-secret",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, String?>.mutate(key: String, value: String?): Map<String, String?> {
|
||||||
|
return toMutableMap().apply { put(key, value) }
|
||||||
|
}
|
||||||
57
libraries/pushproviders/unifiedpush/build.gradle.kts
Normal file
57
libraries/pushproviders/unifiedpush/build.gradle.kts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
kotlin("plugin.serialization") version "1.8.10"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.push.providers.unifiedpush"
|
||||||
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.dagger)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
||||||
|
implementation(projects.libraries.pushstore.api)
|
||||||
|
implementation(projects.libraries.pushproviders.api)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.services.toolbox.api)
|
||||||
|
|
||||||
|
implementation(projects.libraries.network)
|
||||||
|
implementation(platform(libs.network.okhttp.bom))
|
||||||
|
implementation("com.squareup.okhttp3:okhttp")
|
||||||
|
implementation(libs.network.retrofit)
|
||||||
|
|
||||||
|
implementation(libs.serialization.json)
|
||||||
|
|
||||||
|
// UnifiedPush library
|
||||||
|
api(libs.unifiedpush)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2023 New Vector Ltd
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<application>
|
||||||
|
<receiver
|
||||||
|
android:name=".VectorUnifiedPushMessagingReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="ExportedReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
|
||||||
|
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
|
||||||
|
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
|
||||||
|
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
|
||||||
|
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
<receiver
|
||||||
|
android:name=".KeepInternalDistributor"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<!--
|
||||||
|
This action is checked to track installed and uninstalled distributors.
|
||||||
|
We declare it to keep the background sync as an internal
|
||||||
|
unifiedpush distributor.
|
||||||
|
-->
|
||||||
|
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.unifiedpush
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.unifiedpush
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -14,12 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.unifiedpush
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
|
||||||
import io.element.android.libraries.matrix.api.core.asEventId
|
import io.element.android.libraries.matrix.api.core.asEventId
|
||||||
import io.element.android.libraries.matrix.api.core.asRoomId
|
import io.element.android.libraries.matrix.api.core.asRoomId
|
||||||
import io.element.android.libraries.push.impl.push.PushData
|
import io.element.android.libraries.push.providers.api.PushData
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -41,24 +40,28 @@ import kotlinx.serialization.Serializable
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PushDataUnifiedPush(
|
data class PushDataUnifiedPush(
|
||||||
val notification: PushDataUnifiedPushNotification?
|
val notification: PushDataUnifiedPushNotification? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PushDataUnifiedPushNotification(
|
data class PushDataUnifiedPushNotification(
|
||||||
@SerialName("event_id") val eventId: String?,
|
@SerialName("event_id") val eventId: String? = null,
|
||||||
@SerialName("room_id") val roomId: String?,
|
@SerialName("room_id") val roomId: String? = null,
|
||||||
@SerialName("counts") var counts: PushDataUnifiedPushCounts?,
|
@SerialName("counts") var counts: PushDataUnifiedPushCounts? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PushDataUnifiedPushCounts(
|
data class PushDataUnifiedPushCounts(
|
||||||
@SerialName("unread") val unread: Int?
|
@SerialName("unread") val unread: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
fun PushDataUnifiedPush.toPushData() = PushData(
|
fun PushDataUnifiedPush.toPushData(clientSecret: String): PushData? {
|
||||||
eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(),
|
val safeEventId = notification?.eventId?.asEventId() ?: return null
|
||||||
roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(),
|
val safeRoomId = notification.roomId?.asRoomId() ?: return null
|
||||||
unread = notification?.counts?.unread,
|
return PushData(
|
||||||
clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush
|
eventId = safeEventId,
|
||||||
)
|
roomId = safeRoomId,
|
||||||
|
unread = notification.counts?.unread,
|
||||||
|
clientSecret = clientSecret
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -14,55 +14,60 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.unifiedpush
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.push.impl.config.PushConfig
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.push.providers.api.Distributor
|
||||||
|
import io.element.android.libraries.push.providers.api.PusherSubscriber
|
||||||
import org.unifiedpush.android.connector.UnifiedPush
|
import org.unifiedpush.android.connector.UnifiedPush
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RegisterUnifiedPushUseCase @Inject constructor(
|
class RegisterUnifiedPushUseCase @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
|
private val pusherSubscriber: PusherSubscriber,
|
||||||
|
private val unifiedPushStore: UnifiedPushStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
sealed interface RegisterUnifiedPushResult {
|
sealed interface RegisterUnifiedPushResult {
|
||||||
object Success : RegisterUnifiedPushResult
|
object Success : RegisterUnifiedPushResult
|
||||||
object NeedToAskUserForDistributor : RegisterUnifiedPushResult
|
object NeedToAskUserForDistributor : RegisterUnifiedPushResult
|
||||||
|
object Error : RegisterUnifiedPushResult
|
||||||
}
|
}
|
||||||
|
|
||||||
fun execute(distributor: String = ""): RegisterUnifiedPushResult {
|
suspend fun execute(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String): RegisterUnifiedPushResult {
|
||||||
if (distributor.isNotEmpty()) {
|
val distributorValue = distributor.value
|
||||||
saveAndRegisterApp(distributor)
|
if (distributorValue.isNotEmpty()) {
|
||||||
return RegisterUnifiedPushResult.Success
|
saveAndRegisterApp(distributorValue, clientSecret)
|
||||||
}
|
val endpoint = unifiedPushStore.getEndpoint(clientSecret) ?: return RegisterUnifiedPushResult.Error
|
||||||
|
val gateway = unifiedPushStore.getPushGateway(clientSecret) ?: return RegisterUnifiedPushResult.Error
|
||||||
if (!PushConfig.allowExternalUnifiedPushDistributors) {
|
pusherSubscriber.registerPusher(matrixClient, endpoint, gateway)
|
||||||
saveAndRegisterApp(context.packageName)
|
|
||||||
return RegisterUnifiedPushResult.Success
|
return RegisterUnifiedPushResult.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Below should never happen?
|
||||||
if (UnifiedPush.getDistributor(context).isNotEmpty()) {
|
if (UnifiedPush.getDistributor(context).isNotEmpty()) {
|
||||||
registerApp()
|
registerApp(clientSecret)
|
||||||
return RegisterUnifiedPushResult.Success
|
return RegisterUnifiedPushResult.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
val distributors = UnifiedPush.getDistributors(context)
|
val distributors = UnifiedPush.getDistributors(context)
|
||||||
|
|
||||||
return if (distributors.size == 1) {
|
return if (distributors.size == 1) {
|
||||||
saveAndRegisterApp(distributors.first())
|
saveAndRegisterApp(distributors.first(), clientSecret)
|
||||||
RegisterUnifiedPushResult.Success
|
RegisterUnifiedPushResult.Success
|
||||||
} else {
|
} else {
|
||||||
RegisterUnifiedPushResult.NeedToAskUserForDistributor
|
RegisterUnifiedPushResult.NeedToAskUserForDistributor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveAndRegisterApp(distributor: String) {
|
private fun saveAndRegisterApp(distributor: String, clientSecret: String) {
|
||||||
UnifiedPush.saveDistributor(context, distributor)
|
UnifiedPush.saveDistributor(context, distributor)
|
||||||
registerApp()
|
registerApp(clientSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerApp() {
|
private fun registerApp(clientSecret: String) {
|
||||||
UnifiedPush.registerApp(context)
|
UnifiedPush.registerApp(context = context, instance = clientSecret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
|
object UnifiedPushConfig {
|
||||||
|
/**
|
||||||
|
* It is the push gateway for UnifiedPush.
|
||||||
|
* Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify'
|
||||||
|
*/
|
||||||
|
const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
|
||||||
|
|
||||||
|
const val index = 1
|
||||||
|
const val name = "UnifiedPush"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.network.RetrofitFactory
|
||||||
|
import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URL
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class UnifiedPushGatewayResolver @Inject constructor(
|
||||||
|
private val retrofitFactory: RetrofitFactory,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) {
|
||||||
|
suspend fun getGateway(endpoint: String): String? {
|
||||||
|
val gateway = UnifiedPushConfig.default_push_gateway_http_url
|
||||||
|
val url = URL(endpoint)
|
||||||
|
val custom = "${url.protocol}://${url.host}/_matrix/push/v1/notify"
|
||||||
|
Timber.i("Testing $custom")
|
||||||
|
try {
|
||||||
|
return withContext(coroutineDispatchers.io) {
|
||||||
|
val api = retrofitFactory.create("${url.protocol}://${url.host}")
|
||||||
|
.create(UnifiedPushApi::class.java)
|
||||||
|
try {
|
||||||
|
val discoveryResponse = api.discover()
|
||||||
|
if (discoveryResponse.unifiedpush.gateway == "matrix") {
|
||||||
|
Timber.d("Using custom gateway")
|
||||||
|
return@withContext custom
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Timber.tag("UnifiedPushHelper").e(throwable)
|
||||||
|
}
|
||||||
|
return@withContext gateway
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Timber.d(e, "Cannot try custom gateway")
|
||||||
|
}
|
||||||
|
return gateway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.push.providers.api.PusherSubscriber
|
||||||
|
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||||
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new endpoint received from UnifiedPush. Will update all the sessions which are using UnifiedPush as a push provider.
|
||||||
|
*/
|
||||||
|
class UnifiedPushNewGatewayHandler @Inject constructor(
|
||||||
|
private val pusherSubscriber: PusherSubscriber,
|
||||||
|
private val userPushStoreFactory: UserPushStoreFactory,
|
||||||
|
private val pushClientSecret: PushClientSecret,
|
||||||
|
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||||
|
) {
|
||||||
|
suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String) {
|
||||||
|
// Register the pusher for the session with this client secret, if is it using UnifiedPush.
|
||||||
|
val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Unit.also {
|
||||||
|
Timber.w("Unable to retrieve session")
|
||||||
|
}
|
||||||
|
val userDataStore = userPushStoreFactory.create(userId)
|
||||||
|
if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) {
|
||||||
|
matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client ->
|
||||||
|
pusherSubscriber.registerPusher(client, endpoint, pushGateway)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,16 +14,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.unifiedpush
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.push.impl.push.PushData
|
import io.element.android.libraries.push.providers.api.PushData
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class UnifiedPushParser @Inject constructor() {
|
class UnifiedPushParser @Inject constructor() {
|
||||||
fun parse(message: ByteArray): PushData? {
|
private val json by lazy { Json { ignoreUnknownKeys = true } }
|
||||||
return tryOrNull { Json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData()
|
|
||||||
|
fun parse(message: ByteArray, clientSecret: String): PushData? {
|
||||||
|
return tryOrNull { json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData(clientSecret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.element.android.libraries.androidutils.system.getApplicationLabel
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.push.providers.api.Distributor
|
||||||
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
|
import org.unifiedpush.android.connector.UnifiedPush
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class UnifiedPushProvider @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
|
||||||
|
private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
|
||||||
|
private val pushClientSecret: PushClientSecret,
|
||||||
|
) : PushProvider {
|
||||||
|
override val index = UnifiedPushConfig.index
|
||||||
|
override val name = UnifiedPushConfig.name
|
||||||
|
|
||||||
|
override fun getDistributors(): List<Distributor> {
|
||||||
|
val distributors = UnifiedPush.getDistributors(context)
|
||||||
|
return distributors.mapNotNull {
|
||||||
|
if (it == context.packageName) {
|
||||||
|
// Exclude self
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Distributor(it, context.getApplicationLabel(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) {
|
||||||
|
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
|
||||||
|
registerUnifiedPushUseCase.execute(matrixClient, distributor, clientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unregister(matrixClient: MatrixClient) {
|
||||||
|
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
|
||||||
|
unRegisterUnifiedPushUseCase.execute(clientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun troubleshoot(): Result<Unit> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
|
@ -23,9 +23,6 @@ import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.DefaultPreferences
|
import io.element.android.libraries.di.DefaultPreferences
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO EAx Store in BDD (for multisession).
|
|
||||||
*/
|
|
||||||
class UnifiedPushStore @Inject constructor(
|
class UnifiedPushStore @Inject constructor(
|
||||||
@ApplicationContext val context: Context,
|
@ApplicationContext val context: Context,
|
||||||
@DefaultPreferences private val defaultPrefs: SharedPreferences,
|
@DefaultPreferences private val defaultPrefs: SharedPreferences,
|
||||||
|
|
@ -33,40 +30,44 @@ class UnifiedPushStore @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* Retrieves the UnifiedPush Endpoint.
|
* Retrieves the UnifiedPush Endpoint.
|
||||||
*
|
*
|
||||||
|
* @param clientSecret the client secret, to identify the session
|
||||||
* @return the UnifiedPush Endpoint or null if not received
|
* @return the UnifiedPush Endpoint or null if not received
|
||||||
*/
|
*/
|
||||||
fun getEndpoint(): String? {
|
fun getEndpoint(clientSecret: String): String? {
|
||||||
return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null)
|
return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store UnifiedPush Endpoint to the SharedPrefs.
|
* Store UnifiedPush Endpoint to the SharedPrefs.
|
||||||
*
|
*
|
||||||
* @param endpoint the endpoint to store
|
* @param endpoint the endpoint to store
|
||||||
|
* @param clientSecret the client secret, to identify the session
|
||||||
*/
|
*/
|
||||||
fun storeUpEndpoint(endpoint: String?) {
|
fun storeUpEndpoint(endpoint: String?, clientSecret: String) {
|
||||||
defaultPrefs.edit {
|
defaultPrefs.edit {
|
||||||
putString(PREFS_ENDPOINT_OR_TOKEN, endpoint)
|
putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the Push Gateway.
|
* Retrieves the Push Gateway.
|
||||||
*
|
*
|
||||||
|
* @param clientSecret the client secret, to identify the session
|
||||||
* @return the Push Gateway or null if not defined
|
* @return the Push Gateway or null if not defined
|
||||||
*/
|
*/
|
||||||
fun getPushGateway(): String? {
|
fun getPushGateway(clientSecret: String): String? {
|
||||||
return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null)
|
return defaultPrefs.getString(PREFS_PUSH_GATEWAY + clientSecret, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store Push Gateway to the SharedPrefs.
|
* Store Push Gateway to the SharedPrefs.
|
||||||
*
|
*
|
||||||
* @param gateway the push gateway to store
|
* @param gateway the push gateway to store
|
||||||
|
* @param clientSecret the client secret, to identify the session
|
||||||
*/
|
*/
|
||||||
fun storePushGateway(gateway: String?) {
|
fun storePushGateway(gateway: String?, clientSecret: String) {
|
||||||
defaultPrefs.edit {
|
defaultPrefs.edit {
|
||||||
putString(PREFS_PUSH_GATEWAY, gateway)
|
putString(PREFS_PUSH_GATEWAY + clientSecret, gateway)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,39 +14,34 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.unifiedpush
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.push.api.model.BackgroundSyncMode
|
|
||||||
import io.element.android.libraries.push.api.store.PushDataStore
|
|
||||||
import io.element.android.libraries.push.impl.PushersManager
|
|
||||||
import io.element.android.libraries.push.impl.UnifiedPushHelper
|
|
||||||
import io.element.android.libraries.push.impl.UnifiedPushStore
|
|
||||||
import org.unifiedpush.android.connector.UnifiedPush
|
import org.unifiedpush.android.connector.UnifiedPush
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class UnregisterUnifiedPushUseCase @Inject constructor(
|
class UnregisterUnifiedPushUseCase @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val pushDataStore: PushDataStore,
|
//private val pushDataStore: PushDataStore,
|
||||||
private val unifiedPushStore: UnifiedPushStore,
|
private val unifiedPushStore: UnifiedPushStore,
|
||||||
private val unifiedPushHelper: UnifiedPushHelper,
|
private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun execute(pushersManager: PushersManager?) {
|
suspend fun execute(clientSecret: String /*pushersManager: PushersManager?*/) {
|
||||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
//val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
//pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||||
try {
|
try {
|
||||||
unifiedPushHelper.getEndpointOrToken()?.let {
|
unifiedPushStore.getEndpoint(clientSecret)?.let {
|
||||||
Timber.d("Removing $it")
|
Timber.d("Removing $it")
|
||||||
pushersManager?.unregisterPusher(it)
|
// TODO pushersManager?.unregisterPusher(it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.d(e, "Probably unregistering a non existing pusher")
|
Timber.d(e, "Probably unregistering a non existing pusher")
|
||||||
}
|
}
|
||||||
unifiedPushStore.storeUpEndpoint(null)
|
unifiedPushStore.storeUpEndpoint(null, clientSecret)
|
||||||
unifiedPushStore.storePushGateway(null)
|
unifiedPushStore.storePushGateway(null, clientSecret)
|
||||||
UnifiedPush.unregisterApp(context)
|
UnifiedPush.unregisterApp(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,51 +14,39 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.unifiedpush
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.push.api.model.BackgroundSyncMode
|
import io.element.android.libraries.push.providers.api.PushHandler
|
||||||
import io.element.android.libraries.push.api.store.PushDataStore
|
|
||||||
import io.element.android.libraries.push.impl.PushersManager
|
|
||||||
import io.element.android.libraries.push.impl.UnifiedPushHelper
|
|
||||||
import io.element.android.libraries.push.impl.UnifiedPushStore
|
|
||||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
|
||||||
import io.element.android.libraries.push.impl.push.PushHandler
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.unifiedpush.android.connector.MessagingReceiver
|
import org.unifiedpush.android.connector.MessagingReceiver
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("Unified", pushLoggerTag)
|
private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver")
|
||||||
|
|
||||||
class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||||
@Inject lateinit var pushersManager: PushersManager
|
|
||||||
@Inject lateinit var pushParser: UnifiedPushParser
|
@Inject lateinit var pushParser: UnifiedPushParser
|
||||||
|
|
||||||
//@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
|
||||||
@Inject lateinit var pushDataStore: PushDataStore
|
|
||||||
@Inject lateinit var pushHandler: PushHandler
|
@Inject lateinit var pushHandler: PushHandler
|
||||||
@Inject lateinit var guardServiceStarter: GuardServiceStarter
|
@Inject lateinit var guardServiceStarter: GuardServiceStarter
|
||||||
@Inject lateinit var unifiedPushStore: UnifiedPushStore
|
@Inject lateinit var unifiedPushStore: UnifiedPushStore
|
||||||
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
|
@Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver
|
||||||
|
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob())
|
private val coroutineScope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
super.onReceive(context, intent)
|
|
||||||
// Inject
|
|
||||||
context.applicationContext.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
|
context.applicationContext.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
|
||||||
|
super.onReceive(context, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when message is received.
|
* Called when message is received. The message contains the full POST body of the push message.
|
||||||
*
|
*
|
||||||
* @param context the Android context
|
* @param context the Android context
|
||||||
* @param message the message
|
* @param message the message
|
||||||
|
|
@ -67,48 +55,58 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||||
override fun onMessage(context: Context, message: ByteArray, instance: String) {
|
override fun onMessage(context: Context, message: ByteArray, instance: String) {
|
||||||
Timber.tag(loggerTag.value).d("New message")
|
Timber.tag(loggerTag.value).d("New message")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
pushParser.parse(message)?.let {
|
val pushData = pushParser.parse(message, instance)
|
||||||
pushHandler.handle(it)
|
if (pushData == null) {
|
||||||
} ?: run {
|
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
||||||
Timber.tag(loggerTag.value).w("Invalid received data Json format")
|
} else {
|
||||||
|
pushHandler.handle(pushData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new endpoint is to be used for sending push messages.
|
||||||
|
* You should send the endpoint to your application server and sync for missing notifications.
|
||||||
|
*/
|
||||||
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
||||||
Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint")
|
Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint")
|
||||||
if (pushDataStore.areNotificationEnabledForDevice() /* TODO EAx && activeSessionHolder.hasActiveSession() */) {
|
// If the endpoint has changed
|
||||||
// If the endpoint has changed
|
// or the gateway has changed
|
||||||
// or the gateway has changed
|
if (unifiedPushStore.getEndpoint(instance) != endpoint) {
|
||||||
if (unifiedPushHelper.getEndpointOrToken() != endpoint) {
|
unifiedPushStore.storeUpEndpoint(endpoint, instance)
|
||||||
unifiedPushStore.storeUpEndpoint(endpoint)
|
coroutineScope.launch {
|
||||||
coroutineScope.launch {
|
val gateway = unifiedPushGatewayResolver.getGateway(endpoint)
|
||||||
unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) {
|
unifiedPushStore.storePushGateway(gateway, instance)
|
||||||
unifiedPushHelper.getPushGateway()?.let {
|
gateway?.let { pushGateway ->
|
||||||
coroutineScope.launch {
|
newGatewayHandler.handle(endpoint, pushGateway, instance)
|
||||||
pushersManager.onNewUnifiedPushEndpoint(endpoint, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).i("onNewEndpoint: skipped")
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Timber.tag(loggerTag.value).i("onNewEndpoint: skipped")
|
||||||
}
|
}
|
||||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED
|
|
||||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
|
||||||
guardServiceStarter.stop()
|
guardServiceStarter.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the registration is not possible, eg. no network.
|
||||||
|
*/
|
||||||
override fun onRegistrationFailed(context: Context, instance: String) {
|
override fun onRegistrationFailed(context: Context, instance: String) {
|
||||||
|
Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance")
|
||||||
|
/*
|
||||||
Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show()
|
||||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||||
guardServiceStarter.start()
|
guardServiceStarter.start()
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when this application is unregistered from receiving push messages.
|
||||||
|
*/
|
||||||
override fun onUnregistered(context: Context, instance: String) {
|
override fun onUnregistered(context: Context, instance: String) {
|
||||||
Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
|
Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
|
||||||
|
TODO()
|
||||||
|
/*
|
||||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||||
guardServiceStarter.start()
|
guardServiceStarter.start()
|
||||||
|
|
@ -119,5 +117,6 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||||
Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher")
|
Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.unifiedpush
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.multibindings.IntoSet
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
|
import io.element.android.libraries.push.providers.unifiedpush.UnifiedPushProvider
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(AppScope::class)
|
||||||
|
interface UnifiedPushModule {
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
fun bind(pushProvider: UnifiedPushProvider): PushProvider
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush.network
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DiscoveryResponse(
|
||||||
|
@SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush.network
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DiscoveryUnifiedPush(
|
||||||
|
@SerialName("gateway") val gateway: String = ""
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush.network
|
||||||
|
|
||||||
|
import retrofit2.http.GET
|
||||||
|
|
||||||
|
interface UnifiedPushApi {
|
||||||
|
@GET("_matrix/push/v1/notify")
|
||||||
|
suspend fun discover(): DiscoveryResponse
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.providers.unifiedpush
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.push.providers.api.PushData
|
||||||
|
import io.element.android.tests.testutils.assertNullOrThrow
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class UnifiedPushParserTest {
|
||||||
|
private val aClientSecret = "a-client-secret"
|
||||||
|
private val validData = PushData(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
unread = 1,
|
||||||
|
clientSecret = aClientSecret
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test edge cases UnifiedPush`() {
|
||||||
|
val pushParser = UnifiedPushParser()
|
||||||
|
// Empty string
|
||||||
|
assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull()
|
||||||
|
// Empty Json
|
||||||
|
assertThat(pushParser.parse("{}".toByteArray(), aClientSecret)).isNull()
|
||||||
|
// Bad Json
|
||||||
|
assertThat(pushParser.parse("ABC".toByteArray(), aClientSecret)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test UnifiedPush format`() {
|
||||||
|
val pushParser = UnifiedPushParser()
|
||||||
|
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test empty roomId`() {
|
||||||
|
val pushParser = UnifiedPushParser()
|
||||||
|
assertNullOrThrow {
|
||||||
|
pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test invalid roomId`() {
|
||||||
|
val pushParser = UnifiedPushParser()
|
||||||
|
assertNullOrThrow {
|
||||||
|
pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test empty eventId`() {
|
||||||
|
val pushParser = UnifiedPushParser()
|
||||||
|
assertNullOrThrow {
|
||||||
|
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test invalid eventId`() {
|
||||||
|
val pushParser = UnifiedPushParser()
|
||||||
|
assertNullOrThrow {
|
||||||
|
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val UNIFIED_PUSH_DATA =
|
||||||
|
"{\"notification\":{\"event_id\":\"${AN_EVENT_ID.value}\",\"room_id\":\"${A_ROOM_ID.value}\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}"
|
||||||
|
// TODO Check client secret format?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.mutate(oldValue: String, newValue: String): ByteArray {
|
||||||
|
return replace(oldValue, newValue).toByteArray()
|
||||||
|
}
|
||||||
27
libraries/pushstore/api/build.gradle.kts
Normal file
27
libraries/pushstore/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.pushstore.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
}
|
||||||
|
|
@ -14,27 +14,24 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.userpushstore
|
package io.element.android.libraries.pushstore.api
|
||||||
|
|
||||||
const val NOTIFICATION_METHOD_FIREBASE = "NOTIFICATION_METHOD_FIREBASE"
|
|
||||||
const val NOTIFICATION_METHOD_UNIFIEDPUSH = "NOTIFICATION_METHOD_UNIFIEDPUSH"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store data related to push about a user.
|
* Store data related to push about a user.
|
||||||
*/
|
*/
|
||||||
interface UserPushStore {
|
interface UserPushStore {
|
||||||
/**
|
suspend fun getPushProviderName(): String?
|
||||||
* [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH].
|
suspend fun setPushProviderName(value: String)
|
||||||
*/
|
|
||||||
suspend fun getNotificationMethod(): String
|
|
||||||
|
|
||||||
suspend fun setNotificationMethod(value: String)
|
|
||||||
|
|
||||||
suspend fun getCurrentRegisteredPushKey(): String?
|
suspend fun getCurrentRegisteredPushKey(): String?
|
||||||
|
|
||||||
suspend fun setCurrentRegisteredPushKey(value: String)
|
suspend fun setCurrentRegisteredPushKey(value: String)
|
||||||
|
|
||||||
|
suspend fun areNotificationEnabledForDevice(): Boolean
|
||||||
|
suspend fun setNotificationEnabledForDevice(enabled: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if Pin code is disabled, or if user set the settings to see full notification content.
|
||||||
|
*/
|
||||||
|
fun useCompleteNotificationFormat(): Boolean
|
||||||
|
|
||||||
suspend fun reset()
|
suspend fun reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun UserPushStore.isFirebase(): Boolean = getNotificationMethod() == NOTIFICATION_METHOD_FIREBASE
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.pushstore.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store data related to push about a user.
|
||||||
|
*/
|
||||||
|
interface UserPushStoreFactory {
|
||||||
|
fun create(userId: SessionId): UserPushStore
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.clientsecret
|
package io.element.android.libraries.pushstore.api.clientsecret
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.clientsecret
|
package io.element.android.libraries.pushstore.api.clientsecret
|
||||||
|
|
||||||
interface PushClientSecretFactory {
|
interface PushClientSecretFactory {
|
||||||
fun create(): String
|
fun create(): String
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.clientsecret
|
package io.element.android.libraries.pushstore.api.clientsecret
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
47
libraries/pushstore/impl/build.gradle.kts
Normal file
47
libraries/pushstore/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.push.pushstore.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.dagger)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.pushstore.api)
|
||||||
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
|
implementation(libs.androidx.corektx)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.test.mockk)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.services.appnavstate.test)
|
||||||
|
}
|
||||||
|
|
@ -14,28 +14,34 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.userpushstore
|
package io.element.android.libraries.pushstore.impl
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.asSessionId
|
||||||
|
import io.element.android.libraries.pushstore.api.UserPushStore
|
||||||
|
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
class UserPushStoreFactory @Inject constructor(
|
@ContributesBinding(AppScope::class, boundType = UserPushStoreFactory::class)
|
||||||
|
class DefaultUserPushStoreFactory @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val sessionObserver: SessionObserver,
|
private val sessionObserver: SessionObserver,
|
||||||
) : SessionListener {
|
) : UserPushStoreFactory, SessionListener {
|
||||||
init {
|
init {
|
||||||
observeSessions()
|
observeSessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can have only one class accessing a single data store, so keep a cache of them.
|
// We can have only one class accessing a single data store, so keep a cache of them.
|
||||||
private val cache = mutableMapOf<String, UserPushStore>()
|
private val cache = mutableMapOf<SessionId, UserPushStore>()
|
||||||
fun create(userId: String): UserPushStore {
|
override fun create(userId: SessionId): UserPushStore {
|
||||||
return cache.getOrPut(userId) {
|
return cache.getOrPut(userId) {
|
||||||
UserPushStoreDataStore(
|
UserPushStoreDataStore(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
@ -54,6 +60,6 @@ class UserPushStoreFactory @Inject constructor(
|
||||||
|
|
||||||
override suspend fun onSessionDeleted(userId: String) {
|
override suspend fun onSessionDeleted(userId: String) {
|
||||||
// Delete the store
|
// Delete the store
|
||||||
create(userId).reset()
|
userId.asSessionId()?.let { create(it).reset() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,14 +14,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.userpushstore
|
package io.element.android.libraries.pushstore.impl
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import io.element.android.libraries.core.bool.orTrue
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.pushstore.api.UserPushStore
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,19 +33,20 @@ import kotlinx.coroutines.flow.first
|
||||||
*/
|
*/
|
||||||
class UserPushStoreDataStore(
|
class UserPushStoreDataStore(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
userId: String,
|
userId: SessionId,
|
||||||
) : UserPushStore {
|
) : UserPushStore {
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_store_$userId")
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_store_$userId")
|
||||||
private val notificationMethod = stringPreferencesKey("notificationMethod")
|
private val pushProviderName = stringPreferencesKey("pushProviderName")
|
||||||
private val currentPushKey = stringPreferencesKey("currentPushKey")
|
private val currentPushKey = stringPreferencesKey("currentPushKey")
|
||||||
|
private val notificationEnabled = booleanPreferencesKey("notificationEnabled")
|
||||||
|
|
||||||
override suspend fun getNotificationMethod(): String {
|
override suspend fun getPushProviderName(): String? {
|
||||||
return context.dataStore.data.first()[notificationMethod] ?: NOTIFICATION_METHOD_FIREBASE
|
return context.dataStore.data.first()[pushProviderName]
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setNotificationMethod(value: String) {
|
override suspend fun setPushProviderName(value: String) {
|
||||||
context.dataStore.edit {
|
context.dataStore.edit {
|
||||||
it[notificationMethod] = value
|
it[pushProviderName] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,6 +60,20 @@ class UserPushStoreDataStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun areNotificationEnabledForDevice(): Boolean {
|
||||||
|
return context.dataStore.data.first()[notificationEnabled].orTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setNotificationEnabledForDevice(enabled: Boolean) {
|
||||||
|
context.dataStore.edit {
|
||||||
|
it[notificationEnabled] = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun useCompleteNotificationFormat(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
context.dataStore.edit {
|
context.dataStore.edit {
|
||||||
it.clear()
|
it.clear()
|
||||||
|
|
@ -14,10 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.clientsecret
|
package io.element.android.libraries.pushstore.impl.clientsecret
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -14,11 +14,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.clientsecret
|
package io.element.android.libraries.pushstore.impl.clientsecret
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory
|
||||||
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue