Merge pull request #315 from vector-im/feature/bma/push2
Push - cleanup 1
This commit is contained in:
commit
018a5c540a
119 changed files with 1802 additions and 1037 deletions
|
|
@ -33,7 +33,8 @@ plugins {
|
|||
id("com.google.firebase.appdistribution") version "4.0.0"
|
||||
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
||||
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 {
|
||||
|
|
@ -225,9 +226,6 @@ dependencies {
|
|||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
|
||||
implementation(platform(libs.google.firebase.bom))
|
||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
||||
|
||||
implementation(libs.dagger)
|
||||
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.matrix.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,10 @@ class LoggedInPresenter @Inject constructor(
|
|||
override fun present(): LoggedInState {
|
||||
LaunchedEffect(Unit) {
|
||||
// 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()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ fun LoggedInView(
|
|||
state = state.permissionsState,
|
||||
modifier = modifier,
|
||||
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.noop.NoopPermissionsPresenter
|
||||
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.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -55,7 +57,11 @@ class LoggedInPresenterTest {
|
|||
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() {
|
||||
|
|
|
|||
|
|
@ -230,7 +230,6 @@ koverMerged {
|
|||
target = kotlinx.kover.api.VerificationTarget.CLASS
|
||||
overrideClassFilter {
|
||||
includes += "*Presenter"
|
||||
excludes += "*TemplatePresenter"
|
||||
excludes += "*Fake*Presenter"
|
||||
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.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RoomDetailsNode @AssistedInject constructor(
|
||||
|
|
@ -57,7 +56,6 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
activityResultLauncher = null,
|
||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||
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" }
|
||||
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3"
|
||||
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
|
||||
inject = "javax.inject:javax.inject:1"
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import androidx.activity.result.ActivityResultLauncher
|
|||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import io.element.android.libraries.androidutils.R
|
||||
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
|
||||
|
||||
/**
|
||||
|
|
@ -125,7 +126,7 @@ fun startNotificationSettingsIntent(context: Context, activityResultLauncher: Ac
|
|||
|
||||
fun openAppSettingsPage(
|
||||
activity: Activity,
|
||||
noActivityFoundMessage: String,
|
||||
noActivityFoundMessage: String = activity.getString(R.string.error_no_compatible_app_found),
|
||||
) {
|
||||
try {
|
||||
activity.startActivity(
|
||||
|
|
@ -156,7 +157,7 @@ fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String
|
|||
fun startAddGoogleAccountIntent(
|
||||
context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||
noActivityFoundMessage: String,
|
||||
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||
) {
|
||||
val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
|
||||
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
|
||||
|
|
@ -171,7 +172,7 @@ fun startAddGoogleAccountIntent(
|
|||
fun startInstallFromSourceIntent(
|
||||
context: Context,
|
||||
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)
|
||||
.setData(Uri.parse(String.format("package:%s", context.packageName)))
|
||||
|
|
@ -189,7 +190,7 @@ fun startSharePlainTextIntent(
|
|||
text: String,
|
||||
subject: String? = null,
|
||||
extraTitle: String? = null,
|
||||
noActivityFoundMessage: String,
|
||||
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||
) {
|
||||
val share = Intent(Intent.ACTION_SEND)
|
||||
share.type = "text/plain"
|
||||
|
|
@ -217,7 +218,7 @@ fun startSharePlainTextIntent(
|
|||
fun startImportTextFromFileIntent(
|
||||
context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||
noActivityFoundMessage: String,
|
||||
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
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
|
||||
|
||||
import io.element.android.libraries.matrix.api.BuildConfig
|
||||
import java.io.Serializable
|
||||
|
||||
@JvmInline
|
||||
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
|
||||
)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
@ -101,6 +109,14 @@ object MatrixPatterns {
|
|||
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.
|
||||
*
|
||||
|
|
@ -134,6 +150,14 @@ object MatrixPatterns {
|
|||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -16,9 +16,14 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.core
|
||||
|
||||
import io.element.android.libraries.matrix.api.BuildConfig
|
||||
import java.io.Serializable
|
||||
|
||||
@JvmInline
|
||||
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
|
||||
|
||||
import io.element.android.libraries.matrix.api.BuildConfig
|
||||
|
||||
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
|
||||
|
||||
import io.element.android.libraries.matrix.api.BuildConfig
|
||||
import java.io.Serializable
|
||||
|
||||
@JvmInline
|
||||
|
|
@ -26,4 +27,8 @@ value class SpaceId(val value: String) : Serializable
|
|||
*/
|
||||
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
|
||||
|
||||
import io.element.android.libraries.matrix.api.BuildConfig
|
||||
import java.io.Serializable
|
||||
|
||||
@JvmInline
|
||||
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
|
||||
|
||||
import io.element.android.libraries.matrix.api.BuildConfig
|
||||
import java.io.Serializable
|
||||
|
||||
@JvmInline
|
||||
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 {
|
||||
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_SESSION_ID = SessionId(A_USER_ID.value)
|
||||
val A_SESSION_ID_2 = SessionId(A_USER_ID_2.value)
|
||||
val A_SPACE_ID = SpaceId("!aSpaceId")
|
||||
val A_ROOM_ID = RoomId("!aRoomId")
|
||||
val A_ROOM_ID_2 = RoomId("!aRoomId2")
|
||||
val A_SPACE_ID = SpaceId("!aSpaceId:domain")
|
||||
val A_ROOM_ID = RoomId("!aRoomId:domain")
|
||||
val A_ROOM_ID_2 = RoomId("!aRoomId2:domain")
|
||||
val A_THREAD_ID = ThreadId("\$aThreadId")
|
||||
val AN_EVENT_ID = EventId("\$anEventId")
|
||||
val AN_EVENT_ID_2 = EventId("\$anEventId2")
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@ import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
|
|||
|
||||
class FakePushersService : PushersService {
|
||||
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit)
|
||||
override suspend fun unsetHttpPusher(): Result<Unit> = Result.success(Unit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -27,4 +28,6 @@ dependencies {
|
|||
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,48 +32,52 @@ fun PermissionsView(
|
|||
) {
|
||||
if (state.showDialog.not()) return
|
||||
|
||||
if (state.permissionGranted) {
|
||||
// Notification Granted, nothing to do
|
||||
} else if (state.permissionAlreadyDenied) {
|
||||
// In this case, tell the user to go to the settings
|
||||
ConfirmationDialog(
|
||||
modifier = modifier,
|
||||
title = "System",
|
||||
content = "In order to let the application display notification, please grant the permission to the system settings",
|
||||
submitText = "Open settings",
|
||||
onSubmitClicked = {
|
||||
state.eventSink.invoke(PermissionsEvents.CloseDialog)
|
||||
openSystemSettings()
|
||||
},
|
||||
onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) },
|
||||
)
|
||||
} else {
|
||||
val textToShow = if (state.shouldShowRationale) {
|
||||
// TODO Move to state
|
||||
// If the user has denied the permission but the rationale can be shown,
|
||||
// then gently explain why the app requires this permission
|
||||
// permissions_rationale_msg_notification
|
||||
"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."
|
||||
} else {
|
||||
// TODO Move to state
|
||||
// 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."
|
||||
when {
|
||||
state.permissionGranted -> {
|
||||
// Notification Granted, nothing to do
|
||||
}
|
||||
state.permissionAlreadyDenied -> {
|
||||
// In this case, tell the user to go to the settings
|
||||
ConfirmationDialog(
|
||||
modifier = modifier,
|
||||
title = "System",
|
||||
content = "In order to let the application display notification, please grant the permission to the system settings",
|
||||
submitText = "Open settings",
|
||||
onSubmitClicked = {
|
||||
state.eventSink.invoke(PermissionsEvents.CloseDialog)
|
||||
openSystemSettings()
|
||||
},
|
||||
onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) },
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val textToShow = if (state.shouldShowRationale) {
|
||||
// TODO Move to state
|
||||
// If the user has denied the permission but the rationale can be shown,
|
||||
// then gently explain why the app requires this permission
|
||||
// permissions_rationale_msg_notification
|
||||
"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."
|
||||
} else {
|
||||
// TODO Move to state
|
||||
// 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>
|
||||
get() = sequenceOf(
|
||||
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.AssistedFactory
|
||||
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.permissions.api.PermissionsEvents
|
||||
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 timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("DefaultPermissionsPresenter")
|
||||
|
||||
class DefaultPermissionsPresenter @AssistedInject constructor(
|
||||
@Assisted val permission: String,
|
||||
private val permissionsStore: PermissionsStore,
|
||||
|
|
@ -71,7 +74,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
|||
var permissionState: PermissionState? = null
|
||||
|
||||
fun onPermissionResult(result: Boolean) {
|
||||
Timber.tag("PERMISSION").w("onPermissionResult: $result")
|
||||
Timber.tag(loggerTag.value).d("onPermissionResult: $result")
|
||||
localCoroutineScope.launch {
|
||||
permissionsStore.setPermissionAsked(permission, true)
|
||||
}
|
||||
|
|
@ -79,7 +82,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
|||
if (!result) {
|
||||
// Should show rational true -> denied.
|
||||
if (permissionState?.status?.shouldShowRationale == true) {
|
||||
Timber.tag("PERMISSION").w("onPermissionResult: reset the store")
|
||||
Timber.tag(loggerTag.value).d("onPermissionResult: setPermissionDenied to true")
|
||||
localCoroutineScope.launch {
|
||||
permissionsStore.setPermissionDenied(permission, true)
|
||||
}
|
||||
|
|
@ -102,7 +105,6 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
|||
val showDialog = rememberSaveable { mutableStateOf(permissionState.status !is PermissionStatus.Granted) }
|
||||
|
||||
fun handleEvents(event: PermissionsEvents) {
|
||||
Timber.tag("PERMISSION").w("New event: $event")
|
||||
when (event) {
|
||||
PermissionsEvents.CloseDialog -> {
|
||||
showDialog.value = false
|
||||
|
|
@ -123,7 +125,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
|||
permissionAlreadyDenied = isAlreadyDenied,
|
||||
eventSink = ::handleEvents
|
||||
).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.coroutines.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,22 @@
|
|||
package io.element.android.libraries.push.api
|
||||
|
||||
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 {
|
||||
// TODO Move away
|
||||
fun notificationStyleChanged()
|
||||
|
||||
// Ensure pusher is registered
|
||||
suspend fun registerFirebasePusher(matrixClient: MatrixClient)
|
||||
fun getAvailablePushProviders(): List<PushProvider>
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
import io.element.android.libraries.push.api.model.BackgroundSyncMode
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PushDataStore {
|
||||
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.network)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.libraries.pushproviders.api)
|
||||
api(projects.libraries.pushstore.api)
|
||||
api(projects.libraries.push.api)
|
||||
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.appnavstate.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")
|
||||
}
|
||||
|
||||
implementation(platform(libs.google.firebase.bom))
|
||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
||||
|
||||
// UnifiedPush
|
||||
api("com.github.UnifiedPush:android-connector:2.1.1")
|
||||
// TODO Temporary use the deprecated LocalBroadcastManager, to be changed later.
|
||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.mockk)
|
||||
|
|
|
|||
|
|
@ -14,63 +14,15 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<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
|
||||
android:name=".notifications.TestNotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".notifications.NotificationBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -20,27 +20,41 @@ import com.squareup.anvil.annotations.ContributesBinding
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.impl.config.PushConfig
|
||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
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
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushService @Inject constructor(
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val pushersManager: PushersManager,
|
||||
private val fcmHelper: FcmHelper,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val pushProviders: Set<@JvmSuppressWildcards PushProvider>,
|
||||
) : PushService {
|
||||
override fun notificationStyleChanged() {
|
||||
notificationDrawerManager.notificationStyleChanged()
|
||||
}
|
||||
|
||||
override suspend fun registerFirebasePusher(matrixClient: MatrixClient) {
|
||||
val pushKey = fcmHelper.getFcmToken() ?: return Unit.also {
|
||||
Timber.tag(pushLoggerTag.value).w("Unable to register pusher, Firebase token is not known.")
|
||||
override fun getAvailablePushProviders(): List<PushProvider> {
|
||||
return pushProviders.sortedBy { it.index }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
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.auth.MatrixAuthenticationService
|
||||
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.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.log.pushLoggerTag
|
||||
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
|
||||
import io.element.android.libraries.push.impl.userpushstore.UserPushStoreFactory
|
||||
import io.element.android.libraries.push.impl.userpushstore.isFirebase
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.api.toUserList
|
||||
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 io.element.android.services.toolbox.api.appname.AppNameProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -38,62 +37,32 @@ internal const val DEFAULT_PUSHER_FILE_TAG = "mobile"
|
|||
|
||||
private val loggerTag = LoggerTag("PushersManager", pushLoggerTag)
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class PushersManager @Inject constructor(
|
||||
private val unifiedPushHelper: UnifiedPushHelper,
|
||||
// private val localeProvider: LocaleProvider,
|
||||
private val appNameProvider: AppNameProvider,
|
||||
// private val getDeviceInfoUseCase: GetDeviceInfoUseCase,
|
||||
private val pushGatewayNotifyRequest: PushGatewayNotifyRequest,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val sessionStore: SessionStore,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val fcmHelper: FcmHelper,
|
||||
) {
|
||||
) : PusherSubscriber {
|
||||
// TODO Move this to the PushProvider API
|
||||
suspend fun testPush() {
|
||||
pushGatewayNotifyRequest.execute(
|
||||
PushGatewayNotifyRequest.Params(
|
||||
url = unifiedPushHelper.getPushGateway() ?: return,
|
||||
url = "TODO", // unifiedPushHelper.getPushGateway() ?: return,
|
||||
appId = PushConfig.pusher_app_id,
|
||||
pushKey = unifiedPushHelper.getEndpointOrToken().orEmpty(),
|
||||
pushKey = "TODO", // unifiedPushHelper.getEndpointOrToken().orEmpty(),
|
||||
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.
|
||||
*/
|
||||
suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
|
||||
val userDataStore = userPushStoreFactory.create(matrixClient.sessionId.value)
|
||||
override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
|
||||
val userDataStore = userPushStoreFactory.create(matrixClient.sessionId)
|
||||
if (userDataStore.getCurrentRegisteredPushKey() == pushKey) {
|
||||
Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher")
|
||||
} else {
|
||||
|
|
@ -162,9 +131,8 @@ class PushersManager @Inject constructor(
|
|||
// currentSession.pushersService().removeEmailPusher(email)
|
||||
}
|
||||
|
||||
suspend fun unregisterPusher(pushKey: String) {
|
||||
// val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
// currentSession.pushersService().removeHttpPusher(pushKey, PushConfig.pusher_app_id)
|
||||
override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
|
||||
matrixClient.pushersService().unsetHttpPusher()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
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 val firstThrottler = FirstThrottler(200)
|
||||
|
||||
private var useCompleteNotificationFormat = pushDataStore.useCompleteNotificationFormat()
|
||||
// TODO EAx add a setting per user for this
|
||||
private var useCompleteNotificationFormat = true
|
||||
|
||||
init {
|
||||
handlerThread.start()
|
||||
|
|
@ -111,12 +112,6 @@ class NotificationDrawerManager @Inject constructor(
|
|||
}
|
||||
|
||||
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) {
|
||||
Timber.d("onNotifiableEventReceived(): $notifiableEvent")
|
||||
} else {
|
||||
|
|
@ -185,7 +180,7 @@ class NotificationDrawerManager @Inject constructor(
|
|||
// TODO EAx Must be per account
|
||||
fun notificationStyleChanged() {
|
||||
updateEvents {
|
||||
val newSettings = pushDataStore.useCompleteNotificationFormat()
|
||||
val newSettings = true // pushDataStore.useCompleteNotificationFormat()
|
||||
if (newSettings != useCompleteNotificationFormat) {
|
||||
// Settings has changed, remove all current notifications
|
||||
notificationRenderer.cancelAllNotifications()
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ data class NotificationEventQueue constructor(
|
|||
* Acts as a notification debouncer to stop already dismissed push notifications from
|
||||
* 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>
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -21,19 +21,22 @@ import android.content.Intent
|
|||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
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.clientsecret.PushClientSecret
|
||||
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.NotificationActionIds
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
|
@ -43,20 +46,20 @@ import javax.inject.Inject
|
|||
|
||||
private val loggerTag = LoggerTag("PushHandler", pushLoggerTag)
|
||||
|
||||
class PushHandler @Inject constructor(
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushHandler @Inject constructor(
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val notifiableEventResolver: NotifiableEventResolver,
|
||||
private val pushDataStore: PushDataStore,
|
||||
private val defaultPushDataStore: DefaultPushDataStore,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val actionIds: NotificationActionIds,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
) {
|
||||
) : PushHandler {
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob())
|
||||
private val wifiDetector: WifiDetector = WifiDetector(context)
|
||||
|
||||
// UI handler
|
||||
private val mUIHandler by lazy {
|
||||
|
|
@ -68,7 +71,7 @@ class PushHandler @Inject constructor(
|
|||
*
|
||||
* @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")
|
||||
|
||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||
|
|
@ -84,12 +87,6 @@ class PushHandler @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
// TODO EAx Should be per user
|
||||
if (!pushDataStore.areNotificationEnabledForDevice()) {
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device")
|
||||
return
|
||||
}
|
||||
|
||||
mUIHandler.post {
|
||||
coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) }
|
||||
}
|
||||
|
|
@ -134,6 +131,13 @@ class PushHandler @Inject constructor(
|
|||
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)
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||
|
|
@ -17,20 +17,15 @@
|
|||
package io.element.android.libraries.push.impl.store
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
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.ApplicationContext
|
||||
import io.element.android.libraries.di.DefaultPreferences
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -42,7 +37,6 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
|
|||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushDataStore @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@DefaultPreferences private val defaultPrefs: SharedPreferences,
|
||||
) : PushDataStore {
|
||||
private val pushCounter = intPreferencesKey("push_counter")
|
||||
|
||||
|
|
@ -56,94 +50,4 @@ class DefaultPushDataStore @Inject constructor(
|
|||
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.
|
||||
*/
|
||||
|
||||
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.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.
|
||||
*
|
||||
* @property eventId The Event ID. If not null, it will not be empty, and will have a valid format.
|
||||
* @property roomId The Room 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.
|
||||
* @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(
|
||||
val eventId: EventId?,
|
||||
val roomId: RoomId?,
|
||||
val unread: Int?,
|
||||
val clientSecret: String?,
|
||||
val eventId: EventId,
|
||||
val roomId: RoomId,
|
||||
val unread: Int?,
|
||||
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.
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
// TODO
|
||||
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
|
||||
private val unifiedPushHelper: UnifiedPushHelper,
|
||||
private val fcmHelper: FcmHelper,
|
||||
// private val unifiedPushHelper: UnifiedPushHelper,
|
||||
// private val fcmHelper: FcmHelper,
|
||||
// private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
|
||||
if (unifiedPushHelper.isEmbeddedDistributor()) {
|
||||
fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
|
||||
}
|
||||
}
|
||||
// fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
|
||||
// if (unifiedPushHelper.isEmbeddedDistributor()) {
|
||||
// fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,18 +14,17 @@
|
|||
* 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.impl.push.PushData
|
||||
import io.element.android.libraries.push.providers.api.PushData
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirebasePushParser @Inject constructor() {
|
||||
fun parse(message: Map<String, String?>): PushData {
|
||||
fun parse(message: Map<String, String?>): PushData? {
|
||||
val pushDataFirebase = PushDataFirebase(
|
||||
eventId = message["event_id"],
|
||||
roomId = message["room_id"],
|
||||
unread = message["unread"]?.let { tryOrNull { Integer.parseInt(it) } },
|
||||
unread = message["unread"]?.toIntOrNull(),
|
||||
clientSecret = message["cs"],
|
||||
)
|
||||
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.
|
||||
*/
|
||||
|
||||
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.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:
|
||||
|
|
@ -41,9 +40,13 @@ data class PushDataFirebase(
|
|||
val clientSecret: String?
|
||||
)
|
||||
|
||||
fun PushDataFirebase.toPushData() = PushData(
|
||||
eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(),
|
||||
roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(),
|
||||
unread = unread,
|
||||
clientSecret = clientSecret,
|
||||
)
|
||||
fun PushDataFirebase.toPushData(): PushData? {
|
||||
val safeEventId = eventId?.asEventId() ?: return null
|
||||
val safeRoomId = roomId?.asRoomId() ?: return null
|
||||
return PushData(
|
||||
eventId = safeEventId,
|
||||
roomId = safeRoomId,
|
||||
unread = unread,
|
||||
clientSecret = clientSecret,
|
||||
)
|
||||
}
|
||||
|
|
@ -14,25 +14,23 @@
|
|||
* 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.RemoteMessage
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.push.impl.PushersManager
|
||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||
import io.element.android.libraries.push.impl.push.PushHandler
|
||||
import io.element.android.libraries.push.providers.api.PushHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("Firebase", pushLoggerTag)
|
||||
private val loggerTag = LoggerTag("Firebase")
|
||||
|
||||
class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
@Inject lateinit var pushersManager: PushersManager
|
||||
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
|
||||
@Inject lateinit var pushParser: FirebasePushParser
|
||||
@Inject lateinit var pushHandler: PushHandler
|
||||
|
||||
|
|
@ -46,15 +44,18 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||
override fun onNewToken(token: String) {
|
||||
Timber.tag(loggerTag.value).d("New Firebase token")
|
||||
coroutineScope.launch {
|
||||
pushersManager.onNewFirebaseToken(token)
|
||||
firebaseNewTokenHandler.handle(token)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
Timber.tag(loggerTag.value).d("New Firebase message")
|
||||
coroutineScope.launch {
|
||||
pushParser.parse(message.data).let {
|
||||
pushHandler.handle(it)
|
||||
val pushData = pushParser.parse(message.data)
|
||||
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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.firebase
|
||||
package io.element.android.libraries.push.providers.firebase
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.unifiedpush
|
||||
package io.element.android.libraries.push.providers.unifiedpush
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.Context
|
||||
|
|
@ -14,12 +14,11 @@
|
|||
* 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.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.Serializable
|
||||
|
||||
|
|
@ -41,24 +40,28 @@ import kotlinx.serialization.Serializable
|
|||
*/
|
||||
@Serializable
|
||||
data class PushDataUnifiedPush(
|
||||
val notification: PushDataUnifiedPushNotification?
|
||||
val notification: PushDataUnifiedPushNotification? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushDataUnifiedPushNotification(
|
||||
@SerialName("event_id") val eventId: String?,
|
||||
@SerialName("room_id") val roomId: String?,
|
||||
@SerialName("counts") var counts: PushDataUnifiedPushCounts?,
|
||||
@SerialName("event_id") val eventId: String? = null,
|
||||
@SerialName("room_id") val roomId: String? = null,
|
||||
@SerialName("counts") var counts: PushDataUnifiedPushCounts? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushDataUnifiedPushCounts(
|
||||
@SerialName("unread") val unread: Int?
|
||||
@SerialName("unread") val unread: Int? = null
|
||||
)
|
||||
|
||||
fun PushDataUnifiedPush.toPushData() = PushData(
|
||||
eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(),
|
||||
roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(),
|
||||
unread = notification?.counts?.unread,
|
||||
clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush
|
||||
)
|
||||
fun PushDataUnifiedPush.toPushData(clientSecret: String): PushData? {
|
||||
val safeEventId = notification?.eventId?.asEventId() ?: return null
|
||||
val safeRoomId = notification.roomId?.asRoomId() ?: return null
|
||||
return PushData(
|
||||
eventId = safeEventId,
|
||||
roomId = safeRoomId,
|
||||
unread = notification.counts?.unread,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
}
|
||||
|
|
@ -14,55 +14,60 @@
|
|||
* 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 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 javax.inject.Inject
|
||||
|
||||
class RegisterUnifiedPushUseCase @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
) {
|
||||
|
||||
sealed interface RegisterUnifiedPushResult {
|
||||
object Success : RegisterUnifiedPushResult
|
||||
object NeedToAskUserForDistributor : RegisterUnifiedPushResult
|
||||
object Error : RegisterUnifiedPushResult
|
||||
}
|
||||
|
||||
fun execute(distributor: String = ""): RegisterUnifiedPushResult {
|
||||
if (distributor.isNotEmpty()) {
|
||||
saveAndRegisterApp(distributor)
|
||||
return RegisterUnifiedPushResult.Success
|
||||
}
|
||||
|
||||
if (!PushConfig.allowExternalUnifiedPushDistributors) {
|
||||
saveAndRegisterApp(context.packageName)
|
||||
suspend fun execute(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String): RegisterUnifiedPushResult {
|
||||
val distributorValue = distributor.value
|
||||
if (distributorValue.isNotEmpty()) {
|
||||
saveAndRegisterApp(distributorValue, clientSecret)
|
||||
val endpoint = unifiedPushStore.getEndpoint(clientSecret) ?: return RegisterUnifiedPushResult.Error
|
||||
val gateway = unifiedPushStore.getPushGateway(clientSecret) ?: return RegisterUnifiedPushResult.Error
|
||||
pusherSubscriber.registerPusher(matrixClient, endpoint, gateway)
|
||||
return RegisterUnifiedPushResult.Success
|
||||
}
|
||||
|
||||
// TODO Below should never happen?
|
||||
if (UnifiedPush.getDistributor(context).isNotEmpty()) {
|
||||
registerApp()
|
||||
registerApp(clientSecret)
|
||||
return RegisterUnifiedPushResult.Success
|
||||
}
|
||||
|
||||
val distributors = UnifiedPush.getDistributors(context)
|
||||
|
||||
return if (distributors.size == 1) {
|
||||
saveAndRegisterApp(distributors.first())
|
||||
saveAndRegisterApp(distributors.first(), clientSecret)
|
||||
RegisterUnifiedPushResult.Success
|
||||
} else {
|
||||
RegisterUnifiedPushResult.NeedToAskUserForDistributor
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAndRegisterApp(distributor: String) {
|
||||
private fun saveAndRegisterApp(distributor: String, clientSecret: String) {
|
||||
UnifiedPush.saveDistributor(context, distributor)
|
||||
registerApp()
|
||||
registerApp(clientSecret)
|
||||
}
|
||||
|
||||
private fun registerApp() {
|
||||
UnifiedPush.registerApp(context)
|
||||
private fun registerApp(clientSecret: String) {
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,16 +14,18 @@
|
|||
* 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.push.impl.push.PushData
|
||||
import io.element.android.libraries.push.providers.api.PushData
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import javax.inject.Inject
|
||||
|
||||
class UnifiedPushParser @Inject constructor() {
|
||||
fun parse(message: ByteArray): PushData? {
|
||||
return tryOrNull { Json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData()
|
||||
private val json by lazy { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.SharedPreferences
|
||||
|
|
@ -23,9 +23,6 @@ import io.element.android.libraries.di.ApplicationContext
|
|||
import io.element.android.libraries.di.DefaultPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* TODO EAx Store in BDD (for multisession).
|
||||
*/
|
||||
class UnifiedPushStore @Inject constructor(
|
||||
@ApplicationContext val context: Context,
|
||||
@DefaultPreferences private val defaultPrefs: SharedPreferences,
|
||||
|
|
@ -33,40 +30,44 @@ class UnifiedPushStore @Inject constructor(
|
|||
/**
|
||||
* Retrieves the UnifiedPush Endpoint.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @return the UnifiedPush Endpoint or null if not received
|
||||
*/
|
||||
fun getEndpoint(): String? {
|
||||
return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null)
|
||||
fun getEndpoint(clientSecret: String): String? {
|
||||
return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store UnifiedPush Endpoint to the SharedPrefs.
|
||||
*
|
||||
* @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 {
|
||||
putString(PREFS_ENDPOINT_OR_TOKEN, endpoint)
|
||||
putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Push Gateway.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @return the Push Gateway or null if not defined
|
||||
*/
|
||||
fun getPushGateway(): String? {
|
||||
return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null)
|
||||
fun getPushGateway(clientSecret: String): String? {
|
||||
return defaultPrefs.getString(PREFS_PUSH_GATEWAY + clientSecret, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store Push Gateway to the SharedPrefs.
|
||||
*
|
||||
* @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 {
|
||||
putString(PREFS_PUSH_GATEWAY, gateway)
|
||||
putString(PREFS_PUSH_GATEWAY + clientSecret, gateway)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -14,39 +14,34 @@
|
|||
* 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 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 timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class UnregisterUnifiedPushUseCase @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val pushDataStore: PushDataStore,
|
||||
//private val pushDataStore: PushDataStore,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val unifiedPushHelper: UnifiedPushHelper,
|
||||
private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver,
|
||||
) {
|
||||
|
||||
suspend fun execute(pushersManager: PushersManager?) {
|
||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||
suspend fun execute(clientSecret: String /*pushersManager: PushersManager?*/) {
|
||||
//val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||
//pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||
try {
|
||||
unifiedPushHelper.getEndpointOrToken()?.let {
|
||||
unifiedPushStore.getEndpoint(clientSecret)?.let {
|
||||
Timber.d("Removing $it")
|
||||
pushersManager?.unregisterPusher(it)
|
||||
// TODO pushersManager?.unregisterPusher(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.d(e, "Probably unregistering a non existing pusher")
|
||||
}
|
||||
unifiedPushStore.storeUpEndpoint(null)
|
||||
unifiedPushStore.storePushGateway(null)
|
||||
unifiedPushStore.storeUpEndpoint(null, clientSecret)
|
||||
unifiedPushStore.storePushGateway(null, clientSecret)
|
||||
UnifiedPush.unregisterApp(context)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,51 +14,39 @@
|
|||
* 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.Intent
|
||||
import android.widget.Toast
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
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 io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||
import io.element.android.libraries.push.impl.push.PushHandler
|
||||
import io.element.android.libraries.push.providers.api.PushHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.unifiedpush.android.connector.MessagingReceiver
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("Unified", pushLoggerTag)
|
||||
private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver")
|
||||
|
||||
class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||
@Inject lateinit var pushersManager: PushersManager
|
||||
@Inject lateinit var pushParser: UnifiedPushParser
|
||||
|
||||
//@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var pushDataStore: PushDataStore
|
||||
@Inject lateinit var pushHandler: PushHandler
|
||||
@Inject lateinit var guardServiceStarter: GuardServiceStarter
|
||||
@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())
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
super.onReceive(context, intent)
|
||||
// Inject
|
||||
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 message the message
|
||||
|
|
@ -67,48 +55,58 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
override fun onMessage(context: Context, message: ByteArray, instance: String) {
|
||||
Timber.tag(loggerTag.value).d("New message")
|
||||
coroutineScope.launch {
|
||||
pushParser.parse(message)?.let {
|
||||
pushHandler.handle(it)
|
||||
} ?: run {
|
||||
Timber.tag(loggerTag.value).w("Invalid received data Json format")
|
||||
val pushData = pushParser.parse(message, instance)
|
||||
if (pushData == null) {
|
||||
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
||||
} 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) {
|
||||
Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint")
|
||||
if (pushDataStore.areNotificationEnabledForDevice() /* TODO EAx && activeSessionHolder.hasActiveSession() */) {
|
||||
// If the endpoint has changed
|
||||
// or the gateway has changed
|
||||
if (unifiedPushHelper.getEndpointOrToken() != endpoint) {
|
||||
unifiedPushStore.storeUpEndpoint(endpoint)
|
||||
coroutineScope.launch {
|
||||
unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) {
|
||||
unifiedPushHelper.getPushGateway()?.let {
|
||||
coroutineScope.launch {
|
||||
pushersManager.onNewUnifiedPushEndpoint(endpoint, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the endpoint has changed
|
||||
// or the gateway has changed
|
||||
if (unifiedPushStore.getEndpoint(instance) != endpoint) {
|
||||
unifiedPushStore.storeUpEndpoint(endpoint, instance)
|
||||
coroutineScope.launch {
|
||||
val gateway = unifiedPushGatewayResolver.getGateway(endpoint)
|
||||
unifiedPushStore.storePushGateway(gateway, instance)
|
||||
gateway?.let { pushGateway ->
|
||||
newGatewayHandler.handle(endpoint, pushGateway, instance)
|
||||
}
|
||||
} 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the registration is not possible, eg. no network.
|
||||
*/
|
||||
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()
|
||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||
guardServiceStarter.start()
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this application is unregistered from receiving push messages.
|
||||
*/
|
||||
override fun onUnregistered(context: Context, instance: String) {
|
||||
Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
|
||||
TODO()
|
||||
/*
|
||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||
guardServiceStarter.start()
|
||||
|
|
@ -119,5 +117,6 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher")
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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 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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.userpushstore
|
||||
|
||||
const val NOTIFICATION_METHOD_FIREBASE = "NOTIFICATION_METHOD_FIREBASE"
|
||||
const val NOTIFICATION_METHOD_UNIFIEDPUSH = "NOTIFICATION_METHOD_UNIFIEDPUSH"
|
||||
package io.element.android.libraries.pushstore.api
|
||||
|
||||
/**
|
||||
* Store data related to push about a user.
|
||||
*/
|
||||
interface UserPushStore {
|
||||
/**
|
||||
* [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH].
|
||||
*/
|
||||
suspend fun getNotificationMethod(): String
|
||||
|
||||
suspend fun setNotificationMethod(value: String)
|
||||
|
||||
suspend fun getPushProviderName(): String?
|
||||
suspend fun setPushProviderName(value: String)
|
||||
suspend fun getCurrentRegisteredPushKey(): 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 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.
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.clientsecret
|
||||
package io.element.android.libraries.pushstore.api.clientsecret
|
||||
|
||||
interface PushClientSecretFactory {
|
||||
fun create(): String
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.userpushstore
|
||||
package io.element.android.libraries.pushstore.impl
|
||||
|
||||
import android.content.Context
|
||||
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.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.SessionObserver
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
class UserPushStoreFactory @Inject constructor(
|
||||
@ContributesBinding(AppScope::class, boundType = UserPushStoreFactory::class)
|
||||
class DefaultUserPushStoreFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sessionObserver: SessionObserver,
|
||||
) : SessionListener {
|
||||
) : UserPushStoreFactory, SessionListener {
|
||||
init {
|
||||
observeSessions()
|
||||
}
|
||||
|
||||
// We can have only one class accessing a single data store, so keep a cache of them.
|
||||
private val cache = mutableMapOf<String, UserPushStore>()
|
||||
fun create(userId: String): UserPushStore {
|
||||
private val cache = mutableMapOf<SessionId, UserPushStore>()
|
||||
override fun create(userId: SessionId): UserPushStore {
|
||||
return cache.getOrPut(userId) {
|
||||
UserPushStoreDataStore(
|
||||
context = context,
|
||||
|
|
@ -54,6 +60,6 @@ class UserPushStoreFactory @Inject constructor(
|
|||
|
||||
override suspend fun onSessionDeleted(userId: String) {
|
||||
// Delete the store
|
||||
create(userId).reset()
|
||||
userId.asSessionId()?.let { create(it).reset() }
|
||||
}
|
||||
}
|
||||
|
|
@ -14,14 +14,18 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.userpushstore
|
||||
package io.element.android.libraries.pushstore.impl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
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
|
||||
|
||||
/**
|
||||
|
|
@ -29,19 +33,20 @@ import kotlinx.coroutines.flow.first
|
|||
*/
|
||||
class UserPushStoreDataStore(
|
||||
private val context: Context,
|
||||
userId: String,
|
||||
userId: SessionId,
|
||||
) : UserPushStore {
|
||||
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 notificationEnabled = booleanPreferencesKey("notificationEnabled")
|
||||
|
||||
override suspend fun getNotificationMethod(): String {
|
||||
return context.dataStore.data.first()[notificationMethod] ?: NOTIFICATION_METHOD_FIREBASE
|
||||
override suspend fun getPushProviderName(): String? {
|
||||
return context.dataStore.data.first()[pushProviderName]
|
||||
}
|
||||
|
||||
override suspend fun setNotificationMethod(value: String) {
|
||||
override suspend fun setPushProviderName(value: String) {
|
||||
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() {
|
||||
context.dataStore.edit {
|
||||
it.clear()
|
||||
|
|
@ -14,10 +14,11 @@
|
|||
* 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 io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -14,11 +14,14 @@
|
|||
* 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 io.element.android.libraries.di.AppScope
|
||||
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
|
||||
|
||||
@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