commit
6c8f982cd3
190 changed files with 9770 additions and 49 deletions
|
|
@ -33,6 +33,7 @@ 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")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -213,15 +214,20 @@ dependencies {
|
|||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.startup)
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(libs.coil)
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
49
app/src/debug/google-services.json
Normal file
49
app/src/debug/google-services.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"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,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2022 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
|
@ -31,9 +30,10 @@
|
|||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:theme="@style/Theme.ElementX.Splash"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/Theme.ElementX.Splash"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.x
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -30,6 +31,7 @@ import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
|||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||
import io.element.android.x.di.AppBindings
|
||||
import timber.log.Timber
|
||||
|
||||
class MainActivity : NodeComponentActivity() {
|
||||
|
||||
|
|
@ -52,6 +54,17 @@ class MainActivity : NodeComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when:
|
||||
* - the launcher icon is clicked (if the app is already running);
|
||||
* - a notification is clicked.
|
||||
* - the app is going to background (<- this is strange)
|
||||
*/
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
Timber.w("onNewIntent")
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
bindings<AppBindings>().matrixClientsHolder().onSaveInstanceState(outState)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
package io.element.android.x.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -25,6 +28,7 @@ import io.element.android.libraries.core.meta.BuildMeta
|
|||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
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.x.BuildConfig
|
||||
import io.element.android.x.R
|
||||
|
|
@ -47,6 +51,11 @@ object AppModule {
|
|||
return File(context.filesDir, "sessions")
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesResources(@ApplicationContext context: Context): Resources {
|
||||
return context.resources
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesAppCoroutineScope(): CoroutineScope {
|
||||
|
|
@ -69,6 +78,13 @@ object AppModule {
|
|||
okHttpLoggingLevel = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
@DefaultPreferences
|
||||
fun providesDefaultSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.x.intent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.push.impl.intent.IntentProvider
|
||||
import io.element.android.x.MainActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO EAx change to deep-link.
|
||||
@ContributesBinding(AppScope::class)
|
||||
class IntentProviderImpl @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : IntentProvider {
|
||||
override fun getMainIntent(): Intent {
|
||||
return Intent(context, MainActivity::class.java)
|
||||
}
|
||||
|
||||
override fun getIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): Intent {
|
||||
// TODO Handle deeplink or pass parameters
|
||||
return Intent(context, MainActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,4 +37,4 @@
|
|||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
app/src/release/google-services.json
Normal file
40
app/src/release/google-services.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -41,11 +41,16 @@ dependencies {
|
|||
allFeaturesApi(rootDir)
|
||||
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.permissions.noop)
|
||||
|
||||
implementation(projects.features.verifysession.api)
|
||||
implementation(projects.features.roomdetails.api)
|
||||
implementation(projects.tests.uitests)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import com.bumble.appyx.navmodel.backstack.operation.replace
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.loggedin.LoggedInNode
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
|
|
@ -56,6 +57,7 @@ import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
|
|||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
|
|
@ -125,6 +127,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Permanent : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomList : NavTarget
|
||||
|
||||
|
|
@ -143,6 +148,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Permanent -> {
|
||||
createNode<LoggedInNode>(buildContext)
|
||||
}
|
||||
NavTarget.RoomList -> {
|
||||
val callback = object : RoomListEntryPoint.Callback {
|
||||
override fun onRoomClicked(roomId: RoomId) {
|
||||
|
|
@ -211,11 +219,15 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
// Animate navigation to settings and to a room
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
Box(modifier = modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = Modifier,
|
||||
// Animate navigation to settings and to a room
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
|
||||
PermanentChild(navTarget = NavTarget.Permanent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.appnav.loggedin
|
||||
|
||||
// sealed interface LoggedInEvents {
|
||||
// object MyEvent : LoggedInEvents
|
||||
// }
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class LoggedInNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val loggedInPresenter: LoggedInPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val loggedInState = loggedInPresenter.present()
|
||||
LoggedInView(
|
||||
state = loggedInState,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
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 javax.inject.Inject
|
||||
|
||||
class LoggedInPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val pushService: PushService,
|
||||
) : Presenter<LoggedInState> {
|
||||
|
||||
private val postNotificationPermissionsPresenter by lazy {
|
||||
// Ask for POST_NOTIFICATION PERMISSION on Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
NoopPermissionsPresenter()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): LoggedInState {
|
||||
LaunchedEffect(Unit) {
|
||||
// Ensure pusher is registered
|
||||
pushService.registerFirebasePusher(matrixClient)
|
||||
}
|
||||
|
||||
val permissionsState = postNotificationPermissionsPresenter.present()
|
||||
|
||||
// fun handleEvents(event: LoggedInEvents) {
|
||||
// when (event) {
|
||||
// }
|
||||
// }
|
||||
|
||||
return LoggedInState(
|
||||
permissionsState = permissionsState,
|
||||
// eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.appnav.loggedin
|
||||
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
|
||||
data class LoggedInState(
|
||||
val permissionsState: PermissionsState,
|
||||
// val eventSink: (LoggedInEvents) -> Unit
|
||||
)
|
||||
|
|
@ -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.appnav.loggedin
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState
|
||||
|
||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||
override val values: Sequence<LoggedInState>
|
||||
get() = sequenceOf(
|
||||
aLoggedInState(),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aLoggedInState() = LoggedInState(
|
||||
permissionsState = createDummyPostNotificationPermissionsState(),
|
||||
// eventSink = {}
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.androidutils.system.openAppSettingsPage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.permissions.api.PermissionsView
|
||||
|
||||
@Composable
|
||||
fun LoggedInView(
|
||||
state: LoggedInState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current as? Activity
|
||||
|
||||
PermissionsView(
|
||||
state = state.permissionsState,
|
||||
modifier = modifier,
|
||||
openSystemSettings = {
|
||||
activity?.let { openAppSettingsPage(it, "") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoggedInViewLightPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoggedInViewDarkPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: LoggedInState) {
|
||||
LoggedInView(
|
||||
state = state
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LoggedInPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.permissionsState.permission).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(): LoggedInPresenter {
|
||||
return LoggedInPresenter(
|
||||
matrixClient = FakeMatrixClient(),
|
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permission: String): PermissionsPresenter {
|
||||
return NoopPermissionsPresenter()
|
||||
}
|
||||
},
|
||||
pushService = object : PushService {
|
||||
override fun notificationStyleChanged() {
|
||||
}
|
||||
|
||||
override suspend fun registerFirebasePusher(matrixClient: MatrixClient) {
|
||||
}
|
||||
|
||||
override suspend fun testPush() {
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
|||
buildscript {
|
||||
dependencies {
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
|
||||
classpath("com.google.gms:google-services:4.3.15")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +230,9 @@ koverMerged {
|
|||
target = kotlinx.kover.api.VerificationTarget.CLASS
|
||||
overrideClassFilter {
|
||||
includes += "*Presenter"
|
||||
excludes += "*TemplatePresenter"
|
||||
excludes += "*Fake*Presenter"
|
||||
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
||||
}
|
||||
bound {
|
||||
minValue = 90
|
||||
|
|
@ -246,6 +249,7 @@ koverMerged {
|
|||
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*"
|
||||
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*"
|
||||
excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*"
|
||||
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
|
||||
}
|
||||
bound {
|
||||
minValue = 90
|
||||
|
|
|
|||
|
|
@ -251,7 +251,8 @@ Main libraries and frameworks used in this application:
|
|||
|
||||
- Navigation state with [Appyx](https://bumble-tech.github.io/appyx/). Please
|
||||
watch [this video](https://www.droidcon.com/2022/11/15/model-driven-navigation-with-appyx-from-zero-to-hero/) to learn more about Appyx!
|
||||
- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil)
|
||||
- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil). Please
|
||||
watch [this video](https://www.droidcon.com/2022/06/28/dagger-anvil-learning-to-love-dependency-injection/) to learn more about Anvil!
|
||||
- Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule)
|
||||
|
||||
Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ dependencies {
|
|||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
|
|
@ -61,6 +62,7 @@ dependencies {
|
|||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.libraries.permissions.noop)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ internal fun aRoomListState() = RoomListState(
|
|||
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("@id", "U")),
|
||||
roomList = aRoomListRoomSummaryList(),
|
||||
filter = "filter",
|
||||
eventSink = {},
|
||||
snackbarMessage = null,
|
||||
displayVerificationPrompt = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||
|
|
|
|||
|
|
@ -197,11 +197,13 @@ fun RoomListContent(
|
|||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost (snackbarHostState) { data ->
|
||||
Snackbar(
|
||||
snackbarData = data,
|
||||
)
|
||||
}
|
||||
SnackbarHost(snackbarHostState) { data ->
|
||||
Snackbar(
|
||||
snackbarData = data,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@
|
|||
[versions]
|
||||
# Project
|
||||
android_gradle_plugin = "7.4.2"
|
||||
firebase_gradle_plugin = "3.2.0"
|
||||
kotlin = "1.8.10"
|
||||
ksp = "1.8.10-1.0.9"
|
||||
molecule = "0.8.0"
|
||||
|
||||
# AndroidX
|
||||
material = "1.8.0"
|
||||
corektx = "1.10.0"
|
||||
core = "1.10.0"
|
||||
datastore = "1.0.0"
|
||||
constraintlayout = "2.1.4"
|
||||
recyclerview = "1.3.0"
|
||||
|
|
@ -55,12 +54,14 @@ dependencygraph = "0.10"
|
|||
[libraries]
|
||||
# Project
|
||||
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
|
||||
firebase_gradle_plugin = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebase_gradle_plugin" }
|
||||
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:31.2.3"
|
||||
|
||||
# AndroidX
|
||||
androidx_material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||
androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "corektx" }
|
||||
androidx_core = { module = "androidx.core:core", version.ref = "core" }
|
||||
androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" }
|
||||
androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||
androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
||||
|
|
@ -73,6 +74,7 @@ androidx_security_crypto = "androidx.security:security-crypto:1.0.0"
|
|||
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
|
||||
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
||||
androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" }
|
||||
androidx_preference = "androidx.preference:preference:1.2.0"
|
||||
|
||||
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
|
||||
|
||||
|
|
|
|||
|
|
@ -27,5 +27,6 @@ dependencies {
|
|||
implementation(libs.timber)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.activity.activity)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(projects.libraries.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.androidutils.file
|
||||
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedFile
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import java.io.File
|
||||
|
||||
class EncryptedFileFactory(
|
||||
private val context: Context,
|
||||
) {
|
||||
fun create(file: File): EncryptedFile {
|
||||
// We need to use the same key for all the encrypted files.
|
||||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
return EncryptedFile.Builder(
|
||||
file,
|
||||
context,
|
||||
masterKeyAlias,
|
||||
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
|
||||
).build()
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.androidutils.system
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
|
|
@ -77,6 +78,7 @@ fun Context.getApplicationLabel(packageName: String): String {
|
|||
* Note: If the user finally does not grant the permission, PushManager.isBackgroundSyncAllowed()
|
||||
* will return false and the notification privacy will fallback to "LOW_DETAIL".
|
||||
*/
|
||||
@SuppressLint("BatteryLife")
|
||||
fun requestDisablingBatteryOptimization(activity: Activity, activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
|
|
@ -114,13 +116,30 @@ fun startNotificationSettingsIntent(context: Context, activityResultLauncher: Ac
|
|||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
} else {
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra("app_package", context.packageName)
|
||||
intent.putExtra("app_uid", context.applicationInfo?.uid)
|
||||
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.data = Uri.fromParts("package", context.packageName, null)
|
||||
}
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
fun openAppSettingsPage(
|
||||
activity: Activity,
|
||||
noActivityFoundMessage: String,
|
||||
) {
|
||||
try {
|
||||
activity.startActivity(
|
||||
Intent().apply {
|
||||
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
data = Uri.fromParts("package", activity.packageName, null)
|
||||
}
|
||||
)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity.toast(noActivityFoundMessage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification system settings for the given channel id.
|
||||
*/
|
||||
|
|
|
|||
41
libraries/core/src/main/kotlin/io/element/android/libraries/core/cache/CircularCache.kt
vendored
Normal file
41
libraries/core/src/main/kotlin/io/element/android/libraries/core/cache/CircularCache.kt
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.core.cache
|
||||
|
||||
/**
|
||||
* A FIFO circular buffer of T.
|
||||
* This class is not thread safe.
|
||||
*/
|
||||
class CircularCache<T : Any>(cacheSize: Int, factory: (Int) -> Array<T?>) {
|
||||
|
||||
companion object {
|
||||
inline fun <reified T : Any> create(cacheSize: Int) = CircularCache(cacheSize) { Array<T?>(cacheSize) { null } }
|
||||
}
|
||||
|
||||
private val cache = factory(cacheSize)
|
||||
private var writeIndex = 0
|
||||
|
||||
fun contains(value: T): Boolean = cache.contains(value)
|
||||
|
||||
fun put(value: T) {
|
||||
if (writeIndex == cache.size) {
|
||||
writeIndex = 0
|
||||
}
|
||||
cache[writeIndex] = value
|
||||
writeIndex++
|
||||
}
|
||||
}
|
||||
71
libraries/core/src/test/kotlin/io/element/android/libraries/core/cache/CircularCacheTest.kt
vendored
Normal file
71
libraries/core/src/test/kotlin/io/element/android/libraries/core/cache/CircularCacheTest.kt
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.core.cache
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class CircularCacheTest {
|
||||
@Test
|
||||
fun `when putting more than cache size then cache is limited to cache size`() {
|
||||
val (cache, internalData) = createIntCache(cacheSize = 3)
|
||||
|
||||
cache.putInOrder(1, 1, 1, 1, 1, 1)
|
||||
|
||||
assertThat(internalData).isEqualTo(arrayOf(1, 1, 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when putting more than cache then acts as FIFO`() {
|
||||
val (cache, internalData) = createIntCache(cacheSize = 3)
|
||||
|
||||
cache.putInOrder(1, 2, 3, 4)
|
||||
|
||||
assertThat(internalData).isEqualTo(arrayOf(4, 2, 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given empty cache when checking if contains key then is false`() {
|
||||
val (cache, _) = createIntCache(cacheSize = 3)
|
||||
|
||||
val result = cache.contains(1)
|
||||
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given cached key when checking if contains key then is true`() {
|
||||
val (cache, _) = createIntCache(cacheSize = 3)
|
||||
|
||||
cache.put(1)
|
||||
val result = cache.contains(1)
|
||||
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
private fun createIntCache(cacheSize: Int): Pair<CircularCache<Int>, Array<Int?>> {
|
||||
var internalData: Array<Int?>? = null
|
||||
val factory: (Int) -> Array<Int?> = {
|
||||
Array<Int?>(it) { null }.also { array -> internalData = array }
|
||||
}
|
||||
return CircularCache(cacheSize, factory) to internalData!!
|
||||
}
|
||||
|
||||
private fun CircularCache<Int>.putInOrder(vararg values: Int) {
|
||||
values.forEach { put(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier annotation class DefaultPreferences
|
||||
|
|
@ -34,4 +34,6 @@ dependencies {
|
|||
implementation(libs.sqlcipher)
|
||||
implementation(libs.sqlite)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
|
||||
implementation(projects.libraries.androidutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.encrypteddb.passphrase
|
|||
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedFile
|
||||
import io.element.android.libraries.androidutils.file.EncryptedFileFactory
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
|
|
@ -25,23 +26,16 @@ import java.security.SecureRandom
|
|||
* Provides a secure passphrase for SQLCipher by generating a random secret and storing it into an [EncryptedFile].
|
||||
* @param context Android [Context], used by [EncryptedFile] for cryptographic operations.
|
||||
* @param file Destination file where the key will be stored.
|
||||
* @param alias Alias of the key used to encrypt & decrypt the [EncryptedFile]'s contents.
|
||||
* @param secretSize Length of the generated secret.
|
||||
*/
|
||||
class RandomSecretPassphraseProvider(
|
||||
private val context: Context,
|
||||
private val file: File,
|
||||
private val alias: String,
|
||||
private val secretSize: Int = 256,
|
||||
) : PassphraseProvider {
|
||||
|
||||
override fun getPassphrase(): ByteArray {
|
||||
val encryptedFile = EncryptedFile.Builder(
|
||||
file,
|
||||
context,
|
||||
alias,
|
||||
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
|
||||
).build()
|
||||
val encryptedFile = EncryptedFileFactory(context).create(file)
|
||||
return if (!file.exists()) {
|
||||
val secret = generateSecret()
|
||||
encryptedFile.openFileOutput().use { it.write(secret) }
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.MediaResolver
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
||||
|
|
@ -36,6 +38,8 @@ interface MatrixClient : Closeable {
|
|||
fun stopSync()
|
||||
fun mediaResolver(): MediaResolver
|
||||
fun sessionVerificationService(): SessionVerificationService
|
||||
fun pushersService(): PushersService
|
||||
fun notificationService(): NotificationService
|
||||
suspend fun logout()
|
||||
suspend fun loadUserDisplayName(): Result<String>
|
||||
suspend fun loadUserAvatarURLString(): Result<String?>
|
||||
|
|
|
|||
|
|
@ -20,3 +20,5 @@ import java.io.Serializable
|
|||
|
||||
@JvmInline
|
||||
value class EventId(val value: String) : Serializable
|
||||
|
||||
fun String.asEventId() = EventId(this)
|
||||
|
|
|
|||
|
|
@ -20,3 +20,5 @@ import java.io.Serializable
|
|||
|
||||
@JvmInline
|
||||
value class RoomId(val value: String) : Serializable
|
||||
|
||||
fun String.asRoomId() = RoomId(this)
|
||||
|
|
|
|||
|
|
@ -17,3 +17,5 @@
|
|||
package io.element.android.libraries.matrix.api.core
|
||||
|
||||
typealias SessionId = UserId
|
||||
|
||||
fun String.asSessionId() = SessionId(this)
|
||||
|
|
|
|||
|
|
@ -25,3 +25,5 @@ value class SpaceId(val value: String) : Serializable
|
|||
* Value to use when no space is selected by the user.
|
||||
*/
|
||||
val MAIN_SPACE = SpaceId("!mainSpace")
|
||||
|
||||
fun String.asSpaceId() = SpaceId(this)
|
||||
|
|
|
|||
|
|
@ -20,3 +20,5 @@ import java.io.Serializable
|
|||
|
||||
@JvmInline
|
||||
value class ThreadId(val value: String) : Serializable
|
||||
|
||||
fun String.asThreadId() = ThreadId(this)
|
||||
|
|
|
|||
|
|
@ -20,3 +20,5 @@ import java.io.Serializable
|
|||
|
||||
@JvmInline
|
||||
value class UserId(val value: String) : Serializable
|
||||
|
||||
fun String.asUserId() = UserId(this)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
|
||||
data class NotificationData(
|
||||
val item: MatrixTimelineItem,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val isNoisy: Boolean,
|
||||
val avatarUrl: String?,
|
||||
)
|
||||
|
|
@ -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.matrix.api.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface NotificationService {
|
||||
suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
||||
}
|
||||
|
|
@ -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.matrix.api.pusher
|
||||
|
||||
interface PushersService {
|
||||
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
|
||||
}
|
||||
|
|
@ -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.matrix.api.pusher
|
||||
|
||||
data class SetHttpPusherData(
|
||||
val pushKey: String,
|
||||
val appId: String,
|
||||
val url: String,
|
||||
val appDisplayName: String,
|
||||
val deviceDisplayName: String,
|
||||
val profileTag: String?,
|
||||
val lang: String,
|
||||
val defaultPayload: String,
|
||||
)
|
||||
|
|
@ -21,11 +21,15 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.MediaResolver
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaResolver
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
||||
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
|
||||
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
||||
import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource
|
||||
import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy
|
||||
|
|
@ -63,6 +67,11 @@ class RustMatrixClient constructor(
|
|||
override val sessionId: UserId = UserId(client.userId())
|
||||
|
||||
private val verificationService = RustSessionVerificationService()
|
||||
private val pushersService = RustPushersService(
|
||||
client = client,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
private val notificationService = RustNotificationService(baseDirectory, dispatchers)
|
||||
private var slidingSyncUpdateJob: Job? = null
|
||||
|
||||
private val clientDelegate = object : ClientDelegate {
|
||||
|
|
@ -189,6 +198,10 @@ class RustMatrixClient constructor(
|
|||
|
||||
override fun sessionVerificationService(): SessionVerificationService = verificationService
|
||||
|
||||
override fun pushersService(): PushersService = pushersService
|
||||
|
||||
override fun notificationService(): NotificationService = notificationService
|
||||
|
||||
override fun startSync() {
|
||||
if (isSyncing.compareAndSet(false, true)) {
|
||||
slidingSyncObserverToken = slidingSync.sync()
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) {
|
||||
sessionStore.getLatestSession()?.userId?.let { UserId(it) }
|
||||
sessionStore.getLatestSession()?.userId?.let { SessionId(it) }
|
||||
}
|
||||
|
||||
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> = withContext(coroutineDispatchers.io) {
|
||||
|
|
|
|||
|
|
@ -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.matrix.impl.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.impl.timeline.MatrixTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||
import org.matrix.rustcomponents.sdk.NotificationItem
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationMapper @Inject constructor() {
|
||||
// TODO Inject and remove duplicate?
|
||||
private val timelineItemFactory = MatrixTimelineItemMapper(
|
||||
virtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
||||
eventTimelineItemMapper = EventTimelineItemMapper(
|
||||
contentMapper = TimelineEventContentMapper(
|
||||
eventMessageMapper = EventMessageMapper()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
fun map(notificationItem: NotificationItem): NotificationData {
|
||||
return notificationItem.use {
|
||||
NotificationData(
|
||||
item = timelineItemFactory.map(it.item),
|
||||
title = it.title,
|
||||
subtitle = it.subtitle,
|
||||
isNoisy = it.isNoisy,
|
||||
avatarUrl = it.avatarUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.matrix.impl.notification
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class RustNotificationService(
|
||||
private val baseDirectory: File,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : NotificationService {
|
||||
private val notificationMapper: NotificationMapper = NotificationMapper()
|
||||
|
||||
override suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?> {
|
||||
return withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
org.matrix.rustcomponents.sdk.NotificationService(
|
||||
basePath = File(baseDirectory, "sessions").absolutePath,
|
||||
userId = userId.value
|
||||
).use {
|
||||
// TODO Not implemented yet, see https://github.com/matrix-org/matrix-rust-sdk/issues/1628
|
||||
it.getNotificationItem(roomId.value, eventId.value)?.let { notificationItem ->
|
||||
notificationMapper.map(notificationItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.matrix.impl.pushers
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.HttpPusherData
|
||||
import org.matrix.rustcomponents.sdk.PushFormat
|
||||
import org.matrix.rustcomponents.sdk.PusherIdentifiers
|
||||
import org.matrix.rustcomponents.sdk.PusherKind
|
||||
|
||||
class RustPushersService(
|
||||
private val client: Client,
|
||||
private val dispatchers: CoroutineDispatchers
|
||||
) : PushersService {
|
||||
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit> {
|
||||
return withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.setPusher(
|
||||
identifiers = PusherIdentifiers(
|
||||
pushkey = setHttpPusherData.pushKey,
|
||||
appId = setHttpPusherData.appId
|
||||
),
|
||||
kind = PusherKind.Http(
|
||||
data = HttpPusherData(
|
||||
url = setHttpPusherData.url,
|
||||
format = PushFormat.EVENT_ID_ONLY,
|
||||
defaultPayload = setHttpPusherData.defaultPayload
|
||||
)
|
||||
),
|
||||
appDisplayName = setHttpPusherData.appDisplayName,
|
||||
deviceDisplayName = setHttpPusherData.deviceDisplayName,
|
||||
profileTag = setHttpPusherData.profileTag,
|
||||
lang = setHttpPusherData.lang
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,11 +21,15 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.MediaResolver
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaResolver
|
||||
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
||||
import io.element.android.libraries.matrix.test.pushers.FakePushersService
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
|
|
@ -36,7 +40,9 @@ class FakeMatrixClient(
|
|||
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
|
||||
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
|
||||
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
|
||||
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService()
|
||||
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||
private val pushersService: FakePushersService = FakePushersService(),
|
||||
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
||||
) : MatrixClient {
|
||||
|
||||
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||
|
|
@ -91,6 +97,10 @@ class FakeMatrixClient(
|
|||
|
||||
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
||||
|
||||
override fun pushersService(): PushersService = pushersService
|
||||
|
||||
override fun notificationService(): NotificationService = notificationService
|
||||
|
||||
override fun onSlidingSyncUpdate() {}
|
||||
|
||||
override fun roomMembershipObserver(): RoomMembershipObserver {
|
||||
|
|
|
|||
|
|
@ -28,11 +28,15 @@ const val A_USER_NAME = "alice"
|
|||
const val A_PASSWORD = "password"
|
||||
|
||||
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_THREAD_ID = ThreadId("\$aThreadId")
|
||||
val AN_EVENT_ID = EventId("\$anEventId")
|
||||
val AN_EVENT_ID_2 = EventId("\$anEventId2")
|
||||
const val A_UNIQUE_ID = "aUniqueId"
|
||||
|
||||
const val A_ROOM_NAME = "A room name"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.matrix.test.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
|
||||
class FakeNotificationService : NotificationService {
|
||||
override suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?> {
|
||||
return Result.success(null)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.matrix.test.pushers
|
||||
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
|
||||
|
||||
class FakePushersService : PushersService {
|
||||
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit)
|
||||
}
|
||||
30
libraries/permissions/api/build.gradle.kts
Normal file
30
libraries/permissions/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.permissions.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
}
|
||||
|
|
@ -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.permissions.api
|
||||
|
||||
sealed interface PermissionsEvents {
|
||||
object OpenSystemDialog : PermissionsEvents
|
||||
object CloseDialog : PermissionsEvents
|
||||
}
|
||||
|
|
@ -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.permissions.api
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
interface PermissionsPresenter : Presenter<PermissionsState> {
|
||||
|
||||
interface Factory {
|
||||
fun create(permission: String): PermissionsPresenter
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.permissions.api
|
||||
|
||||
data class PermissionsState(
|
||||
// For instance Manifest.permission.POST_NOTIFICATIONS
|
||||
val permission: String,
|
||||
val permissionGranted: Boolean,
|
||||
val shouldShowRationale: Boolean,
|
||||
val showDialog: Boolean,
|
||||
val permissionAlreadyAsked: Boolean,
|
||||
// If true, there is no need to ask again, the system dialog will not be displayed
|
||||
val permissionAlreadyDenied: Boolean,
|
||||
val eventSink: (PermissionsEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.permissions.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@Composable
|
||||
fun PermissionsView(
|
||||
state: PermissionsState,
|
||||
modifier: Modifier = Modifier,
|
||||
openSystemSettings: () -> Unit = {},
|
||||
) {
|
||||
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."
|
||||
}
|
||||
ConfirmationDialog(
|
||||
modifier = modifier,
|
||||
title = "Notifications",
|
||||
content = textToShow,
|
||||
submitText = "Request permission",
|
||||
onSubmitClicked = {
|
||||
state.eventSink.invoke(PermissionsEvents.OpenSystemDialog)
|
||||
},
|
||||
onCancelClicked = {
|
||||
state.eventSink.invoke(PermissionsEvents.CloseDialog)
|
||||
},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PermissionsViewLightPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PermissionsViewDarkPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: PermissionsState) {
|
||||
PermissionsView(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.permissions.api
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
open class PermissionsViewStateProvider : PreviewParameterProvider<PermissionsState> {
|
||||
override val values: Sequence<PermissionsState>
|
||||
get() = sequenceOf(
|
||||
aPermissionsState(),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aPermissionsState() = PermissionsState(
|
||||
permission = Manifest.permission.INTERNET,
|
||||
permissionGranted = false,
|
||||
shouldShowRationale = false,
|
||||
showDialog = true,
|
||||
permissionAlreadyAsked = false,
|
||||
permissionAlreadyDenied = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.permissions.api
|
||||
|
||||
fun createDummyPostNotificationPermissionsState() = PermissionsState(
|
||||
permission = "Manifest.permission.POST_NOTIFICATIONS",
|
||||
permissionGranted = true,
|
||||
shouldShowRationale = false,
|
||||
showDialog = false,
|
||||
permissionAlreadyAsked = false,
|
||||
permissionAlreadyDenied = false,
|
||||
eventSink = { }
|
||||
)
|
||||
67
libraries/permissions/impl/build.gradle.kts
Normal file
67
libraries/permissions/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
||||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.permissions.impl"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.anvilannotations)
|
||||
|
||||
implementation(libs.accompanist.permission)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.permissions.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
package io.element.android.libraries.permissions.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
interface PermissionStateProvider {
|
||||
@Composable
|
||||
fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AccompanistPermissionStateProvider @Inject constructor() : PermissionStateProvider {
|
||||
@Composable
|
||||
override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState {
|
||||
return rememberPermissionState(
|
||||
permission = permission,
|
||||
onPermissionResult = onPermissionResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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.permissions.impl
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class DefaultPermissionsPresenter @AssistedInject constructor(
|
||||
@Assisted val permission: String,
|
||||
private val permissionsStore: PermissionsStore,
|
||||
private val permissionStateProvider: PermissionStateProvider,
|
||||
) : PermissionsPresenter {
|
||||
|
||||
@AssistedFactory
|
||||
@ContributesBinding(AppScope::class)
|
||||
interface Factory : PermissionsPresenter.Factory {
|
||||
override fun create(permission: String): DefaultPermissionsPresenter
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@SuppressLint("InlinedApi")
|
||||
@Composable
|
||||
override fun present(): PermissionsState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
// To reset the store: ResetStore()
|
||||
|
||||
val isAlreadyDenied: Boolean by permissionsStore
|
||||
.isPermissionDenied(permission)
|
||||
.collectAsState(initial = false)
|
||||
|
||||
val isAlreadyAsked: Boolean by permissionsStore
|
||||
.isPermissionAsked(permission)
|
||||
.collectAsState(initial = false)
|
||||
|
||||
var permissionState: PermissionState? = null
|
||||
|
||||
fun onPermissionResult(result: Boolean) {
|
||||
Timber.tag("PERMISSION").w("onPermissionResult: $result")
|
||||
localCoroutineScope.launch {
|
||||
permissionsStore.setPermissionAsked(permission, true)
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
// Should show rational true -> denied.
|
||||
if (permissionState?.status?.shouldShowRationale == true) {
|
||||
Timber.tag("PERMISSION").w("onPermissionResult: reset the store")
|
||||
localCoroutineScope.launch {
|
||||
permissionsStore.setPermissionDenied(permission, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
permissionState = permissionStateProvider.provide(
|
||||
permission = permission,
|
||||
onPermissionResult = ::onPermissionResult
|
||||
)
|
||||
|
||||
LaunchedEffect(this) {
|
||||
if (permissionState.status.isGranted) {
|
||||
// User may have granted permission from the settings, to reset the store regarding this permission
|
||||
permissionsStore.resetPermission(permission)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
PermissionsEvents.OpenSystemDialog -> {
|
||||
permissionState.launchPermissionRequest()
|
||||
showDialog.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PermissionsState(
|
||||
permission = permissionState.permission,
|
||||
permissionGranted = permissionState.status.isGranted,
|
||||
shouldShowRationale = permissionState.status.shouldShowRationale,
|
||||
showDialog = showDialog.value,
|
||||
permissionAlreadyAsked = isAlreadyAsked,
|
||||
permissionAlreadyDenied = isAlreadyDenied,
|
||||
eventSink = ::handleEvents
|
||||
).also {
|
||||
Timber.tag("PERMISSION").w("New state: $it")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@Composable
|
||||
private fun ResetStore() {
|
||||
LaunchedEffect(this@DefaultPermissionsPresenter) {
|
||||
launch {
|
||||
permissionsStore.resetStore()
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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.permissions.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.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "permissions_store")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPermissionsStore @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : PermissionsStore {
|
||||
private val store = context.dataStore
|
||||
|
||||
override suspend fun setPermissionDenied(permission: String, value: Boolean) {
|
||||
store.edit { prefs ->
|
||||
prefs[getDeniedPreferenceKey(permission)] = value
|
||||
}
|
||||
}
|
||||
|
||||
override fun isPermissionDenied(permission: String): Flow<Boolean> {
|
||||
return store.data.map {
|
||||
it[getDeniedPreferenceKey(permission)].orFalse()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setPermissionAsked(permission: String, value: Boolean) {
|
||||
store.edit { prefs ->
|
||||
prefs[getAskedPreferenceKey(permission)] = value
|
||||
}
|
||||
}
|
||||
|
||||
override fun isPermissionAsked(permission: String): Flow<Boolean> {
|
||||
return store.data.map {
|
||||
it[getAskedPreferenceKey(permission)].orFalse()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetPermission(permission: String) {
|
||||
setPermissionAsked(permission, false)
|
||||
setPermissionDenied(permission, false)
|
||||
}
|
||||
|
||||
override suspend fun resetStore() {
|
||||
store.edit { it.clear() }
|
||||
}
|
||||
|
||||
private fun getDeniedPreferenceKey(permission: String) = booleanPreferencesKey("${permission}_denied")
|
||||
private fun getAskedPreferenceKey(permission: String) = booleanPreferencesKey("${permission}_asked")
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.permissions.impl
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PermissionsStore {
|
||||
suspend fun setPermissionDenied(permission: String, value: Boolean)
|
||||
fun isPermissionDenied(permission: String): Flow<Boolean>
|
||||
|
||||
suspend fun setPermissionAsked(permission: String, value: Boolean)
|
||||
fun isPermissionAsked(permission: String): Flow<Boolean>
|
||||
|
||||
suspend fun resetPermission(permission: String)
|
||||
|
||||
// To debug
|
||||
suspend fun resetStore()
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalPermissionsApi::class)
|
||||
|
||||
package io.element.android.libraries.permissions.impl
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
const val A_PERMISSION = "A_PERMISSION"
|
||||
|
||||
class DefaultPermissionsPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val permissionsStore = InMemoryPermissionsStore()
|
||||
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Granted)
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionState)
|
||||
val presenter = DefaultPermissionsPresenter(
|
||||
A_PERMISSION,
|
||||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.permission).isEqualTo(A_PERMISSION)
|
||||
assertThat(initialState.permissionGranted).isTrue()
|
||||
assertThat(initialState.shouldShowRationale).isFalse()
|
||||
assertThat(initialState.permissionAlreadyAsked).isFalse()
|
||||
assertThat(initialState.permissionAlreadyDenied).isFalse()
|
||||
assertThat(initialState.showDialog).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user closes dialog`() = runTest {
|
||||
val permissionsStore = InMemoryPermissionsStore()
|
||||
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false))
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionState)
|
||||
val presenter = DefaultPermissionsPresenter(
|
||||
A_PERMISSION,
|
||||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showDialog).isTrue()
|
||||
initialState.eventSink.invoke(PermissionsEvents.CloseDialog)
|
||||
assertThat(awaitItem().showDialog).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user does not grant permission`() = runTest {
|
||||
val permissionsStore = InMemoryPermissionsStore()
|
||||
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false))
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionState)
|
||||
val presenter = DefaultPermissionsPresenter(
|
||||
A_PERMISSION,
|
||||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showDialog).isTrue()
|
||||
initialState.eventSink.invoke(PermissionsEvents.OpenSystemDialog)
|
||||
assertThat(permissionState.launchPermissionRequestCalled).isTrue()
|
||||
assertThat(awaitItem().showDialog).isFalse()
|
||||
// User does not grant permission
|
||||
permissionStateProvider.userGiveAnswer(answer = false, firstTime = true)
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.permissionGranted).isFalse()
|
||||
assertThat(state.showDialog).isFalse()
|
||||
assertThat(state.permissionAlreadyDenied).isFalse()
|
||||
assertThat(state.permissionAlreadyAsked).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user does not grant permission second time`() = runTest {
|
||||
val permissionsStore = InMemoryPermissionsStore()
|
||||
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = true))
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionState)
|
||||
val presenter = DefaultPermissionsPresenter(
|
||||
A_PERMISSION,
|
||||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showDialog).isTrue()
|
||||
initialState.eventSink.invoke(PermissionsEvents.OpenSystemDialog)
|
||||
assertThat(permissionState.launchPermissionRequestCalled).isTrue()
|
||||
assertThat(awaitItem().showDialog).isFalse()
|
||||
// User does not grant permission
|
||||
permissionStateProvider.userGiveAnswer(answer = false, firstTime = false)
|
||||
skipItems(2)
|
||||
val state = awaitItem()
|
||||
assertThat(state.permissionGranted).isFalse()
|
||||
assertThat(state.showDialog).isFalse()
|
||||
assertThat(state.permissionAlreadyDenied).isTrue()
|
||||
assertThat(state.permissionAlreadyAsked).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user does not grant permission third time`() = runTest {
|
||||
val permissionsStore = InMemoryPermissionsStore(permissionDenied = true, permissionAsked = true)
|
||||
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false))
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionState)
|
||||
val presenter = DefaultPermissionsPresenter(
|
||||
A_PERMISSION,
|
||||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showDialog).isTrue()
|
||||
assertThat(initialState.permissionGranted).isFalse()
|
||||
assertThat(initialState.permissionAlreadyDenied).isTrue()
|
||||
assertThat(initialState.permissionAlreadyAsked).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user grants permission`() = runTest {
|
||||
val permissionsStore = InMemoryPermissionsStore()
|
||||
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false))
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionState)
|
||||
val presenter = DefaultPermissionsPresenter(
|
||||
A_PERMISSION,
|
||||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showDialog).isTrue()
|
||||
initialState.eventSink.invoke(PermissionsEvents.OpenSystemDialog)
|
||||
assertThat(permissionState.launchPermissionRequestCalled).isTrue()
|
||||
assertThat(awaitItem().showDialog).isFalse()
|
||||
// User grants permission
|
||||
permissionStateProvider.userGiveAnswer(answer = true, firstTime = true)
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.permissionGranted).isTrue()
|
||||
assertThat(state.showDialog).isFalse()
|
||||
assertThat(state.permissionAlreadyDenied).isFalse()
|
||||
assertThat(state.permissionAlreadyAsked).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
package io.element.android.libraries.permissions.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
|
||||
class FakePermissionStateProvider constructor(
|
||||
private val permissionState: FakePermissionState
|
||||
) : PermissionStateProvider {
|
||||
private lateinit var onPermissionResult: (Boolean) -> Unit
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState {
|
||||
this.onPermissionResult = onPermissionResult
|
||||
return permissionState
|
||||
}
|
||||
|
||||
fun userGiveAnswer(answer: Boolean, firstTime: Boolean) {
|
||||
onPermissionResult.invoke(answer)
|
||||
permissionState.givenPermissionStatus(answer, firstTime)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class FakePermissionState(
|
||||
override val permission: String,
|
||||
initialStatus: PermissionStatus,
|
||||
) : PermissionState {
|
||||
|
||||
override var status: PermissionStatus by mutableStateOf(initialStatus)
|
||||
|
||||
var launchPermissionRequestCalled = false
|
||||
private set
|
||||
|
||||
override fun launchPermissionRequest() {
|
||||
launchPermissionRequestCalled = true
|
||||
}
|
||||
|
||||
fun givenPermissionStatus(hasPermission: Boolean, shouldShowRationale: Boolean) {
|
||||
status = if (hasPermission) PermissionStatus.Granted else PermissionStatus.Denied(shouldShowRationale)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.permissions.impl
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class InMemoryPermissionsStore(
|
||||
permissionDenied: Boolean = false,
|
||||
permissionAsked: Boolean = false,
|
||||
) : PermissionsStore {
|
||||
private val permissionDeniedFlow = MutableStateFlow(permissionDenied)
|
||||
private val permissionAskedFlow = MutableStateFlow(permissionAsked)
|
||||
|
||||
override suspend fun setPermissionDenied(permission: String, value: Boolean) {
|
||||
permissionDeniedFlow.value = value
|
||||
}
|
||||
|
||||
override fun isPermissionDenied(permission: String): Flow<Boolean> = permissionDeniedFlow
|
||||
|
||||
override suspend fun setPermissionAsked(permission: String, value: Boolean) {
|
||||
permissionAskedFlow.value = value
|
||||
}
|
||||
|
||||
override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionAskedFlow
|
||||
|
||||
override suspend fun resetPermission(permission: String) {
|
||||
setPermissionAsked(permission, false)
|
||||
setPermissionDenied(permission, false)
|
||||
}
|
||||
|
||||
override suspend fun resetStore() {
|
||||
}
|
||||
}
|
||||
36
libraries/permissions/noop/build.gradle.kts
Normal file
36
libraries/permissions/noop/build.gradle.kts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
||||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.permissions.noop"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
api(projects.libraries.permissions.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.permissions.noop
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
|
||||
class NoopPermissionsPresenter : PermissionsPresenter {
|
||||
|
||||
@Composable
|
||||
override fun present(): PermissionsState {
|
||||
return PermissionsState(
|
||||
permission = "",
|
||||
permissionGranted = false,
|
||||
shouldShowRationale = false,
|
||||
showDialog = false,
|
||||
permissionAlreadyAsked = false,
|
||||
permissionAlreadyDenied = false,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.permissions.noop
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class NoopPermissionsPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = NoopPermissionsPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.permission).isEmpty()
|
||||
assertThat(initialState.permissionGranted).isFalse()
|
||||
assertThat(initialState.shouldShowRationale).isFalse()
|
||||
assertThat(initialState.permissionAlreadyAsked).isFalse()
|
||||
assertThat(initialState.permissionAlreadyDenied).isFalse()
|
||||
assertThat(initialState.showDialog).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
29
libraries/push/api/build.gradle.kts
Normal file
29
libraries/push/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -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.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
interface PushService {
|
||||
fun notificationStyleChanged()
|
||||
|
||||
// Ensure pusher is registered
|
||||
suspend fun registerFirebasePusher(matrixClient: MatrixClient)
|
||||
|
||||
suspend fun testPush()
|
||||
}
|
||||
|
|
@ -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.api.gateway
|
||||
|
||||
sealed class PushGatewayFailure : Throwable(cause = null) {
|
||||
object PusherRejected : PushGatewayFailure()
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.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
|
||||
}
|
||||
69
libraries/push/impl/build.gradle.kts
Normal file
69
libraries/push/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.matrix.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") {
|
||||
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")
|
||||
|
||||
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)
|
||||
}
|
||||
76
libraries/push/impl/src/main/AndroidManifest.xml
Normal file
76
libraries/push/impl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?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">
|
||||
|
||||
<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>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 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.push.impl.notifications.NotificationDrawerManager
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushService @Inject constructor(
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val pushersManager: PushersManager,
|
||||
private val fcmHelper: FcmHelper,
|
||||
) : 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.")
|
||||
}
|
||||
pushersManager.registerPusher(matrixClient, pushKey, PushConfig.pusher_http_url)
|
||||
}
|
||||
|
||||
override suspend fun testPush() {
|
||||
pushersManager.testPush()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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)
|
||||
*/
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 2018 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright 2019 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 io.element.android.libraries.core.log.logger.LoggerTag
|
||||
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.services.toolbox.api.appname.AppNameProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal const val DEFAULT_PUSHER_FILE_TAG = "mobile"
|
||||
|
||||
private val loggerTag = LoggerTag("PushersManager", pushLoggerTag)
|
||||
|
||||
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,
|
||||
) {
|
||||
suspend fun testPush() {
|
||||
pushGatewayNotifyRequest.execute(
|
||||
PushGatewayNotifyRequest.Params(
|
||||
url = unifiedPushHelper.getPushGateway() ?: return,
|
||||
appId = PushConfig.pusher_app_id,
|
||||
pushKey = 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)
|
||||
if (userDataStore.getCurrentRegisteredPushKey() == pushKey) {
|
||||
Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher")
|
||||
} else {
|
||||
// Register the pusher to the server
|
||||
matrixClient.pushersService().setHttpPusher(
|
||||
createHttpPusher(pushKey, gateway, matrixClient.sessionId)
|
||||
).fold(
|
||||
{
|
||||
userDataStore.setCurrentRegisteredPushKey(pushKey)
|
||||
},
|
||||
{ throwable ->
|
||||
Timber.tag(loggerTag.value).e(throwable, "Unable to register the pusher")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createHttpPusher(
|
||||
pushKey: String,
|
||||
gateway: String,
|
||||
userId: SessionId,
|
||||
): SetHttpPusherData =
|
||||
SetHttpPusherData(
|
||||
pushKey = pushKey,
|
||||
appId = PushConfig.pusher_app_id,
|
||||
profileTag = DEFAULT_PUSHER_FILE_TAG + "_" /* TODO + abs(activeSessionHolder.getActiveSession().myUserId.hashCode())*/,
|
||||
lang = "en", // TODO localeProvider.current().language,
|
||||
appDisplayName = appNameProvider.getAppName(),
|
||||
deviceDisplayName = "MyDevice", // TODO getDeviceInfoUseCase.execute().displayName().orEmpty(),
|
||||
url = gateway,
|
||||
defaultPayload = createDefaultPayload(pushClientSecret.getSecretForUser(userId))
|
||||
)
|
||||
|
||||
/**
|
||||
* Ex: {"cs":"sfvsdv"}
|
||||
*/
|
||||
private fun createDefaultPayload(secretForUser: String): String {
|
||||
return "{\"cs\":\"$secretForUser\"}"
|
||||
}
|
||||
|
||||
suspend fun registerEmailForPush(email: String) {
|
||||
TODO()
|
||||
/*
|
||||
val currentSession = activeSessionHolder.getActiveSession()
|
||||
val appName = appNameProvider.getAppName()
|
||||
currentSession.pushersService().addEmailPusher(
|
||||
email = email,
|
||||
lang = localeProvider.current().language,
|
||||
emailBranding = appName,
|
||||
appDisplayName = appName,
|
||||
deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE"
|
||||
)
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
fun getPusherForCurrentSession() {}/*: Pusher? {
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return null
|
||||
val deviceId = session.sessionParams.deviceId
|
||||
return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
|
||||
}
|
||||
*/
|
||||
|
||||
suspend fun unregisterEmailPusher(email: String) {
|
||||
// val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
// currentSession.pushersService().removeEmailPusher(email)
|
||||
}
|
||||
|
||||
suspend fun unregisterPusher(pushKey: String) {
|
||||
// val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
// currentSession.pushersService().removeHttpPusher(pushKey, PushConfig.pusher_app_id)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
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,
|
||||
) {
|
||||
/**
|
||||
* Retrieves the UnifiedPush Endpoint.
|
||||
*
|
||||
* @return the UnifiedPush Endpoint or null if not received
|
||||
*/
|
||||
fun getEndpoint(): String? {
|
||||
return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store UnifiedPush Endpoint to the SharedPrefs.
|
||||
*
|
||||
* @param endpoint the endpoint to store
|
||||
*/
|
||||
fun storeUpEndpoint(endpoint: String?) {
|
||||
defaultPrefs.edit {
|
||||
putString(PREFS_ENDPOINT_OR_TOKEN, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Push Gateway.
|
||||
*
|
||||
* @return the Push Gateway or null if not defined
|
||||
*/
|
||||
fun getPushGateway(): String? {
|
||||
return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store Push Gateway to the SharedPrefs.
|
||||
*
|
||||
* @param gateway the push gateway to store
|
||||
*/
|
||||
fun storePushGateway(gateway: String?) {
|
||||
defaultPrefs.edit {
|
||||
putString(PREFS_PUSH_GATEWAY, gateway)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN"
|
||||
private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.clientsecret
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface PushClientSecret {
|
||||
/**
|
||||
* To call when registering a pusher. It will return the existing secret or create a new one.
|
||||
*/
|
||||
suspend fun getSecretForUser(userId: SessionId): String
|
||||
|
||||
/**
|
||||
* To call when receiving a push containing a client secret.
|
||||
* Return null if not found.
|
||||
*/
|
||||
suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
|
||||
|
||||
/**
|
||||
* To call when the user signs out.
|
||||
*/
|
||||
suspend fun resetSecretForUser(userId: SessionId)
|
||||
}
|
||||
|
|
@ -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.impl.clientsecret
|
||||
|
||||
interface PushClientSecretFactory {
|
||||
fun create(): String
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.clientsecret
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class PushClientSecretFactoryImpl @Inject constructor() : PushClientSecretFactory {
|
||||
override fun create(): String {
|
||||
return UUID.randomUUID().toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.clientsecret
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class PushClientSecretImpl @Inject constructor(
|
||||
private val pushClientSecretFactory: PushClientSecretFactory,
|
||||
private val pushClientSecretStore: PushClientSecretStore,
|
||||
) : PushClientSecret {
|
||||
override suspend fun getSecretForUser(userId: SessionId): String {
|
||||
val existingSecret = pushClientSecretStore.getSecret(userId)
|
||||
if (existingSecret != null) {
|
||||
return existingSecret
|
||||
}
|
||||
val newSecret = pushClientSecretFactory.create()
|
||||
pushClientSecretStore.storeSecret(userId, newSecret)
|
||||
return newSecret
|
||||
}
|
||||
|
||||
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
|
||||
return pushClientSecretStore.getUserIdFromSecret(clientSecret)
|
||||
}
|
||||
|
||||
override suspend fun resetSecretForUser(userId: SessionId) {
|
||||
pushClientSecretStore.resetSecret(userId)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.push.impl.clientsecret
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface PushClientSecretStore {
|
||||
suspend fun storeSecret(userId: SessionId, clientSecret: String)
|
||||
suspend fun getSecret(userId: SessionId): String?
|
||||
suspend fun resetSecret(userId: SessionId)
|
||||
suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.clientsecret
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
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.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.asSessionId
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_client_secret_store")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class PushClientSecretStoreDataStore @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : PushClientSecretStore {
|
||||
override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
|
||||
context.dataStore.edit { settings ->
|
||||
settings[getPreferenceKeyForUser(userId)] = clientSecret
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSecret(userId: SessionId): String? {
|
||||
return context.dataStore.data.first()[getPreferenceKeyForUser(userId)]
|
||||
}
|
||||
|
||||
override suspend fun resetSecret(userId: SessionId) {
|
||||
context.dataStore.edit { settings ->
|
||||
settings.remove(getPreferenceKeyForUser(userId))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
|
||||
val keyValues = context.dataStore.data.first().asMap()
|
||||
val matchingKey = keyValues.keys.firstOrNull {
|
||||
keyValues[it] == clientSecret
|
||||
}
|
||||
return matchingKey?.name?.asSessionId()
|
||||
}
|
||||
|
||||
private fun getPreferenceKeyForUser(userId: SessionId) = stringPreferencesKey(userId.value)
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.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
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.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
|
||||
|
||||
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
|
||||
/*
|
||||
TODO EAx
|
||||
val currentSession = activeSessionHolder.getActiveSession()
|
||||
val currentPushers = currentSession.pushersService().getPushers()
|
||||
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
|
||||
*/
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.firebase
|
||||
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.push.impl.push.PushData
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirebasePushParser @Inject constructor() {
|
||||
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) } },
|
||||
clientSecret = message["cs"],
|
||||
)
|
||||
return pushDataFirebase.toPushData()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.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
|
||||
|
||||
/**
|
||||
* In this case, the format is:
|
||||
* <pre>
|
||||
* {
|
||||
* "event_id":"$anEventId",
|
||||
* "room_id":"!aRoomId",
|
||||
* "unread":"1",
|
||||
* "prio":"high",
|
||||
* "cs":"<client_secret>"
|
||||
* }
|
||||
* </pre>
|
||||
* .
|
||||
*/
|
||||
data class PushDataFirebase(
|
||||
val eventId: String?,
|
||||
val roomId: String?,
|
||||
var unread: Int?,
|
||||
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,
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.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 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)
|
||||
|
||||
class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
@Inject lateinit var pushersManager: PushersManager
|
||||
@Inject lateinit var pushParser: FirebasePushParser
|
||||
@Inject lateinit var pushHandler: PushHandler
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob())
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
applicationContext.bindings<VectorFirebaseMessagingServiceBindings>().inject(this)
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
Timber.tag(loggerTag.value).d("New Firebase token")
|
||||
coroutineScope.launch {
|
||||
pushersManager.onNewFirebaseToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
Timber.tag(loggerTag.value).d("New Firebase message")
|
||||
coroutineScope.launch {
|
||||
pushParser.parse(message.data).let {
|
||||
pushHandler.handle(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.impl.firebase
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface VectorFirebaseMessagingServiceBindings {
|
||||
fun inject(service: VectorFirebaseMessagingService)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.intent
|
||||
|
||||
import android.content.Intent
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
|
||||
interface IntentProvider {
|
||||
/**
|
||||
* Provide an intent to start the application.
|
||||
*/
|
||||
fun getMainIntent(): Intent
|
||||
|
||||
fun getIntent(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId?,
|
||||
threadId: ThreadId?,
|
||||
): Intent
|
||||
}
|
||||
|
|
@ -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.impl.log
|
||||
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
|
||||
internal val pushLoggerTag = LoggerTag("Push")
|
||||
internal val notificationLoggerTag = LoggerTag("Notification", pushLoggerTag)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 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.notifications
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
class FilteredEventDetector @Inject constructor(
|
||||
//private val activeSessionDataSource: ActiveSessionDataSource
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns true if the given event should be ignored.
|
||||
* Used to skip notifications if a non expected message is received.
|
||||
*/
|
||||
fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean {
|
||||
/* TODO EAx
|
||||
val session = activeSessionDataSource.currentValue?.orNull() ?: return false
|
||||
|
||||
if (notifiableEvent is NotifiableMessageEvent) {
|
||||
val room = session.getRoom(notifiableEvent.roomId) ?: return false
|
||||
val timelineEvent = room.getTimelineEvent(notifiableEvent.eventId) ?: return false
|
||||
return timelineEvent.shouldBeIgnored()
|
||||
}
|
||||
|
||||
*/
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the timeline event should be ignored.
|
||||
*/
|
||||
/*
|
||||
private fun TimelineEvent.shouldBeIgnored(): Boolean {
|
||||
if (root.isVoiceMessage()) {
|
||||
val audioEvent = root.asMessageAudioEvent()
|
||||
// if the event is a voice message related to a voice broadcast, only show the event on the first chunk.
|
||||
return audioEvent.isVoiceBroadcast() && audioEvent?.sequence != 1
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.notifications
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.*
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias ProcessedEvents = List<ProcessedEvent<NotifiableEvent>>
|
||||
|
||||
class NotifiableEventProcessor @Inject constructor(
|
||||
private val outdatedDetector: OutdatedEventDetector,
|
||||
) {
|
||||
|
||||
fun process(
|
||||
queuedEvents: List<NotifiableEvent>,
|
||||
appNavigationState: AppNavigationState?,
|
||||
renderedEvents: ProcessedEvents,
|
||||
): ProcessedEvents {
|
||||
val processedEvents = queuedEvents.map {
|
||||
val type = when (it) {
|
||||
is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP
|
||||
is NotifiableMessageEvent -> when {
|
||||
it.shouldIgnoreMessageEventInRoom(appNavigationState) -> {
|
||||
ProcessedEvent.Type.REMOVE
|
||||
.also { Timber.d("notification message removed due to currently viewing the same room or thread") }
|
||||
}
|
||||
outdatedDetector.isMessageOutdated(it) -> ProcessedEvent.Type.REMOVE
|
||||
.also { Timber.d("notification message removed due to being read") }
|
||||
else -> ProcessedEvent.Type.KEEP
|
||||
}
|
||||
is SimpleNotifiableEvent -> when (it.type) {
|
||||
/*EventType.REDACTION*/ "m.room.redaction" -> ProcessedEvent.Type.REMOVE
|
||||
else -> ProcessedEvent.Type.KEEP
|
||||
}
|
||||
}
|
||||
ProcessedEvent(type, it)
|
||||
}
|
||||
|
||||
val removedEventsDiff = renderedEvents.filter { renderedEvent ->
|
||||
queuedEvents.none { it.eventId == renderedEvent.event.eventId }
|
||||
}.map { ProcessedEvent(ProcessedEvent.Type.REMOVE, it.event) }
|
||||
|
||||
return removedEventsDiff + processedEvents
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright 2019 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.notifications
|
||||
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
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.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("NotifiableEventResolver", pushLoggerTag)
|
||||
|
||||
/**
|
||||
* The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.
|
||||
* It is used as a bridge between the Event Thread and the NotificationDrawerManager.
|
||||
* The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that,
|
||||
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
|
||||
*/
|
||||
class NotifiableEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
// private val noticeEventFormatter: NoticeEventFormatter,
|
||||
// private val displayableEventFormatter: DisplayableEventFormatter,
|
||||
private val clock: SystemClock,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
private val buildMeta: BuildMeta,
|
||||
) {
|
||||
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
|
||||
// Restore session
|
||||
val session = matrixAuthenticationService.restoreSession(sessionId).getOrNull() ?: return null
|
||||
// TODO EAx, no need for a session?
|
||||
val notificationData = session.let {// TODO Use make the app crashes
|
||||
it.notificationService().getNotification(
|
||||
userId = sessionId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
)
|
||||
}.fold(
|
||||
{
|
||||
it
|
||||
},
|
||||
{
|
||||
Timber.tag(loggerTag.value).e(it, "Unable to resolve event.")
|
||||
null
|
||||
}
|
||||
).orDefault(roomId, eventId)
|
||||
|
||||
return notificationData.asNotifiableEvent(sessionId, roomId, eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent {
|
||||
return NotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
noisy = false,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
senderName = null,
|
||||
senderId = null,
|
||||
body = "$eventId in $roomId",
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
roomIsDirect = false,
|
||||
roomAvatarPath = null,
|
||||
senderAvatarPath = null,
|
||||
soundName = null,
|
||||
outGoingMessage = false,
|
||||
outGoingMessageFailed = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO This is a temporary method for EAx
|
||||
*/
|
||||
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
|
||||
return this ?: NotificationData(
|
||||
item = MatrixTimelineItem.Event(
|
||||
event = EventTimelineItem(
|
||||
uniqueIdentifier = eventId.value,
|
||||
eventId = eventId,
|
||||
isEditable = false,
|
||||
isLocal = false,
|
||||
isOwn = false,
|
||||
isRemote = false,
|
||||
localSendState = null,
|
||||
reactions = emptyList(),
|
||||
sender = UserId(""),
|
||||
senderProfile = ProfileTimelineDetails.Unavailable,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
content = MessageContent(
|
||||
body = eventId.value,
|
||||
inReplyTo = null,
|
||||
isEdited = false,
|
||||
type = TextMessageType(
|
||||
body = eventId.value,
|
||||
formatted = null
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
title = roomId.value,
|
||||
subtitle = eventId.value,
|
||||
isNoisy = false,
|
||||
avatarUrl = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2019 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.notifications
|
||||
|
||||
data class NotificationAction(
|
||||
val shouldNotify: Boolean,
|
||||
val highlight: Boolean,
|
||||
val soundName: String?
|
||||
)
|
||||
|
||||
/*
|
||||
fun List<Action>.toNotificationAction(): NotificationAction {
|
||||
var shouldNotify = false
|
||||
var highlight = false
|
||||
var sound: String? = null
|
||||
forEach { action ->
|
||||
when (action) {
|
||||
is Action.Notify -> shouldNotify = true
|
||||
is Action.DoNotNotify -> shouldNotify = false
|
||||
is Action.Highlight -> highlight = action.highlight
|
||||
is Action.Sound -> sound = action.sound
|
||||
}
|
||||
}
|
||||
return NotificationAction(shouldNotify, highlight, sound)
|
||||
}
|
||||
*/
|
||||
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