Merge branch 'develop' into feature/ons/toggle_ip_address_visibility
# Conflicts: # vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
This commit is contained in:
commit
202c0c58ab
|
@ -0,0 +1 @@
|
||||||
|
[Voice Broadcast] Add seekbar in listening tile
|
|
@ -0,0 +1 @@
|
||||||
|
Push notifications toggle: align implementation for current session
|
|
@ -0,0 +1 @@
|
||||||
|
[Metrics] Add `SpannableMetricPlugin` to support spans within transactions.
|
|
@ -0,0 +1 @@
|
||||||
|
Fix description of verified sessions
|
|
@ -8,7 +8,7 @@ ext.versions = [
|
||||||
|
|
||||||
def gradle = "7.3.1"
|
def gradle = "7.3.1"
|
||||||
// Ref: https://kotlinlang.org/releases.html
|
// Ref: https://kotlinlang.org/releases.html
|
||||||
def kotlin = "1.7.20"
|
def kotlin = "1.7.21"
|
||||||
def kotlinCoroutines = "1.6.4"
|
def kotlinCoroutines = "1.6.4"
|
||||||
def dagger = "2.44"
|
def dagger = "2.44"
|
||||||
def appDistribution = "16.0.0-beta05"
|
def appDistribution = "16.0.0-beta05"
|
||||||
|
|
|
@ -103,14 +103,12 @@ class VideoViewHolder constructor(itemView: View) :
|
||||||
views.videoView.setOnPreparedListener {
|
views.videoView.setOnPreparedListener {
|
||||||
stopTimer()
|
stopTimer()
|
||||||
countUpTimer = CountUpTimer(100).also {
|
countUpTimer = CountUpTimer(100).also {
|
||||||
it.tickListener = object : CountUpTimer.TickListener {
|
it.tickListener = CountUpTimer.TickListener {
|
||||||
override fun onTick(milliseconds: Long) {
|
val duration = views.videoView.duration
|
||||||
val duration = views.videoView.duration
|
val progress = views.videoView.currentPosition
|
||||||
val progress = views.videoView.currentPosition
|
val isPlaying = views.videoView.isPlaying
|
||||||
val isPlaying = views.videoView.isPlaying
|
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
||||||
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
it.resume()
|
it.resume()
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ class CountUpTimer(private val intervalInMs: Long = 1_000) {
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TickListener {
|
fun interface TickListener {
|
||||||
fun onTick(milliseconds: Long)
|
fun onTick(milliseconds: Long)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1679,7 +1679,8 @@
|
||||||
<string name="create_new_room">Create New Room</string>
|
<string name="create_new_room">Create New Room</string>
|
||||||
<string name="create_new_space">Create New Space</string>
|
<string name="create_new_space">Create New Space</string>
|
||||||
<string name="error_no_network">No network. Please check your Internet connection.</string>
|
<string name="error_no_network">No network. Please check your Internet connection.</string>
|
||||||
<string name="error_check_network">Something went wrong. Please check your network connection and try again.</string>
|
<!-- TODO delete -->
|
||||||
|
<string name="error_check_network" tools:ignore="UnusedResources">Something went wrong. Please check your network connection and try again.</string>
|
||||||
<string name="change_room_directory_network">"Change network"</string>
|
<string name="change_room_directory_network">"Change network"</string>
|
||||||
<string name="please_wait">"Please wait…"</string>
|
<string name="please_wait">"Please wait…"</string>
|
||||||
<string name="updating_your_data">Updating your data…</string>
|
<string name="updating_your_data">Updating your data…</string>
|
||||||
|
@ -3380,7 +3381,9 @@
|
||||||
<string name="device_manager_learn_more_sessions_unverified_title">Unverified sessions</string>
|
<string name="device_manager_learn_more_sessions_unverified_title">Unverified sessions</string>
|
||||||
<string name="device_manager_learn_more_sessions_unverified">Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.</string>
|
<string name="device_manager_learn_more_sessions_unverified">Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.</string>
|
||||||
<string name="device_manager_learn_more_sessions_verified_title">Verified sessions</string>
|
<string name="device_manager_learn_more_sessions_verified_title">Verified sessions</string>
|
||||||
<string name="device_manager_learn_more_sessions_verified">Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.</string>
|
<!-- TODO TO BE REMOVED -->
|
||||||
|
<string name="device_manager_learn_more_sessions_verified" tools:ignore="UnusedResources">Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.</string>
|
||||||
|
<string name="device_manager_learn_more_sessions_verified_description">Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.</string>
|
||||||
<string name="device_manager_learn_more_session_rename_title">Renaming sessions</string>
|
<string name="device_manager_learn_more_session_rename_title">Renaming sessions</string>
|
||||||
<string name="device_manager_learn_more_session_rename">Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.</string>
|
<string name="device_manager_learn_more_session_rename">Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.</string>
|
||||||
<string name="labs_enable_session_manager_title">Enable new session manager</string>
|
<string name="labs_enable_session_manager_title">Enable new session manager</string>
|
||||||
|
|
|
@ -17,25 +17,51 @@
|
||||||
package org.matrix.android.sdk.api.extensions
|
package org.matrix.android.sdk.api.extensions
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.metrics.MetricPlugin
|
import org.matrix.android.sdk.api.metrics.MetricPlugin
|
||||||
|
import org.matrix.android.sdk.api.metrics.SpannableMetricPlugin
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the given [block] while measuring the transaction.
|
* Executes the given [block] while measuring the transaction.
|
||||||
|
*
|
||||||
|
* @param block Action/Task to be executed within this span.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
inline fun measureMetric(metricMeasurementPlugins: List<MetricPlugin>, block: () -> Unit) {
|
inline fun List<MetricPlugin>.measureMetric(block: () -> Unit) {
|
||||||
contract {
|
contract {
|
||||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
metricMeasurementPlugins.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
|
this.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
|
||||||
block()
|
block()
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
metricMeasurementPlugins.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
|
this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
|
||||||
throw throwable
|
throw throwable
|
||||||
} finally {
|
} finally {
|
||||||
metricMeasurementPlugins.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction.
|
this.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the given [block] while measuring a span.
|
||||||
|
*
|
||||||
|
* @param operation Name of the new span.
|
||||||
|
* @param description Description of the new span.
|
||||||
|
* @param block Action/Task to be executed within this span.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
inline fun List<SpannableMetricPlugin>.measureSpan(operation: String, description: String, block: () -> Unit) {
|
||||||
|
contract {
|
||||||
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.forEach { plugin -> plugin.startSpan(operation, description) } // Start the transaction.
|
||||||
|
block()
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
|
||||||
|
throw throwable
|
||||||
|
} finally {
|
||||||
|
this.forEach { plugin -> plugin.finishSpan() } // Finally, finish this transaction.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.metrics
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A plugin that tracks span along with transactions.
|
||||||
|
*/
|
||||||
|
interface SpannableMetricPlugin : MetricPlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the span for a sub-task.
|
||||||
|
*
|
||||||
|
* @param operation Name of the new span.
|
||||||
|
* @param description Description of the new span.
|
||||||
|
*/
|
||||||
|
fun startSpan(operation: String, description: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish the span when sub-task is completed.
|
||||||
|
*/
|
||||||
|
fun finishSpan()
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.metrics
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("SyncDurationMetricPlugin", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An spannable metric plugin for sync response handling task.
|
||||||
|
*/
|
||||||
|
interface SyncDurationMetricPlugin : SpannableMetricPlugin {
|
||||||
|
|
||||||
|
override fun logTransaction(message: String?) {
|
||||||
|
Timber.tag(loggerTag.value).v("## syncResponseHandler() : $message")
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.homeserver
|
package org.matrix.android.sdk.api.session.homeserver
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface defines a method to retrieve the homeserver capabilities.
|
* This interface defines a method to retrieve the homeserver capabilities.
|
||||||
*/
|
*/
|
||||||
|
@ -30,4 +33,9 @@ interface HomeServerCapabilitiesService {
|
||||||
* Get the HomeServer capabilities.
|
* Get the HomeServer capabilities.
|
||||||
*/
|
*/
|
||||||
fun getHomeServerCapabilities(): HomeServerCapabilities
|
fun getHomeServerCapabilities(): HomeServerCapabilities
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a LiveData on the HomeServer capabilities.
|
||||||
|
*/
|
||||||
|
fun getHomeServerCapabilitiesLive(): LiveData<Optional<HomeServerCapabilities>>
|
||||||
}
|
}
|
||||||
|
|
|
@ -355,7 +355,7 @@ internal class DeviceListManager @Inject constructor(
|
||||||
val relevantPlugins = metricPlugins.filterIsInstance<DownloadDeviceKeysMetricsPlugin>()
|
val relevantPlugins = metricPlugins.filterIsInstance<DownloadDeviceKeysMetricsPlugin>()
|
||||||
|
|
||||||
val response: KeysQueryResponse
|
val response: KeysQueryResponse
|
||||||
measureMetric(relevantPlugins) {
|
relevantPlugins.measureMetric {
|
||||||
response = try {
|
response = try {
|
||||||
downloadKeysForUsersTask.execute(params)
|
downloadKeysForUsersTask.execute(params)
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
|
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.homeserver
|
package org.matrix.android.sdk.internal.session.homeserver
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultHomeServerCapabilitiesService @Inject constructor(
|
internal class DefaultHomeServerCapabilitiesService @Inject constructor(
|
||||||
|
@ -33,4 +35,8 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor(
|
||||||
return homeServerCapabilitiesDataSource.getHomeServerCapabilities()
|
return homeServerCapabilitiesDataSource.getHomeServerCapabilities()
|
||||||
?: HomeServerCapabilities()
|
?: HomeServerCapabilities()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getHomeServerCapabilitiesLive(): LiveData<Optional<HomeServerCapabilities>> {
|
||||||
|
return homeServerCapabilitiesDataSource.getHomeServerCapabilitiesLive()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,14 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.homeserver
|
package org.matrix.android.sdk.internal.session.homeserver
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.where
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper
|
import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper
|
||||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
|
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
|
||||||
import org.matrix.android.sdk.internal.database.query.get
|
import org.matrix.android.sdk.internal.database.query.get
|
||||||
|
@ -26,7 +31,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class HomeServerCapabilitiesDataSource @Inject constructor(
|
internal class HomeServerCapabilitiesDataSource @Inject constructor(
|
||||||
@SessionDatabase private val monarchy: Monarchy
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
) {
|
) {
|
||||||
fun getHomeServerCapabilities(): HomeServerCapabilities? {
|
fun getHomeServerCapabilities(): HomeServerCapabilities? {
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
|
@ -35,4 +40,14 @@ internal class HomeServerCapabilitiesDataSource @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getHomeServerCapabilitiesLive(): LiveData<Optional<HomeServerCapabilities>> {
|
||||||
|
val liveData = monarchy.findAllMappedWithChanges(
|
||||||
|
{ realm: Realm -> realm.where<HomeServerCapabilitiesEntity>() },
|
||||||
|
{ HomeServerCapabilitiesMapper.map(it) }
|
||||||
|
)
|
||||||
|
return Transformations.map(liveData) {
|
||||||
|
it.firstOrNull().toOptional()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
package org.matrix.android.sdk.internal.session.sync
|
package org.matrix.android.sdk.internal.session.sync
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import io.realm.Realm
|
||||||
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
|
import org.matrix.android.sdk.api.extensions.measureMetric
|
||||||
|
import org.matrix.android.sdk.api.extensions.measureSpan
|
||||||
|
import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin
|
||||||
import org.matrix.android.sdk.api.session.pushrules.PushRuleService
|
import org.matrix.android.sdk.api.session.pushrules.PushRuleService
|
||||||
import org.matrix.android.sdk.api.session.pushrules.RuleScope
|
import org.matrix.android.sdk.api.session.pushrules.RuleScope
|
||||||
import org.matrix.android.sdk.api.session.sync.InitialSyncStep
|
import org.matrix.android.sdk.api.session.sync.InitialSyncStep
|
||||||
|
@ -52,9 +57,12 @@ internal class SyncResponseHandler @Inject constructor(
|
||||||
private val tokenStore: SyncTokenStore,
|
private val tokenStore: SyncTokenStore,
|
||||||
private val processEventForPushTask: ProcessEventForPushTask,
|
private val processEventForPushTask: ProcessEventForPushTask,
|
||||||
private val pushRuleService: PushRuleService,
|
private val pushRuleService: PushRuleService,
|
||||||
private val presenceSyncHandler: PresenceSyncHandler
|
private val presenceSyncHandler: PresenceSyncHandler,
|
||||||
|
matrixConfiguration: MatrixConfiguration,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance<SyncDurationMetricPlugin>()
|
||||||
|
|
||||||
suspend fun handleResponse(
|
suspend fun handleResponse(
|
||||||
syncResponse: SyncResponse,
|
syncResponse: SyncResponse,
|
||||||
fromToken: String?,
|
fromToken: String?,
|
||||||
|
@ -63,39 +71,91 @@ internal class SyncResponseHandler @Inject constructor(
|
||||||
val isInitialSync = fromToken == null
|
val isInitialSync = fromToken == null
|
||||||
Timber.v("Start handling sync, is InitialSync: $isInitialSync")
|
Timber.v("Start handling sync, is InitialSync: $isInitialSync")
|
||||||
|
|
||||||
measureTimeMillis {
|
relevantPlugins.measureMetric {
|
||||||
if (!cryptoService.isStarted()) {
|
startCryptoService(isInitialSync)
|
||||||
Timber.v("Should start cryptoService")
|
|
||||||
cryptoService.start()
|
|
||||||
}
|
|
||||||
cryptoService.onSyncWillProcess(isInitialSync)
|
|
||||||
}.also {
|
|
||||||
Timber.v("Finish handling start cryptoService in $it ms")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the to device events before the room ones
|
// Handle the to device events before the room ones
|
||||||
// to ensure to decrypt them properly
|
// to ensure to decrypt them properly
|
||||||
measureTimeMillis {
|
handleToDevice(syncResponse, reporter)
|
||||||
Timber.v("Handle toDevice")
|
|
||||||
reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) {
|
val aggregator = SyncResponsePostTreatmentAggregator()
|
||||||
if (syncResponse.toDevice != null) {
|
|
||||||
cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
|
// Prerequisite for thread events handling in RoomSyncHandler
|
||||||
|
// Disabled due to the new fallback
|
||||||
|
// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||||
|
// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
|
||||||
|
// }
|
||||||
|
|
||||||
|
startMonarchyTransaction(syncResponse, isInitialSync, reporter, aggregator)
|
||||||
|
|
||||||
|
aggregateSyncResponse(aggregator)
|
||||||
|
|
||||||
|
postTreatmentSyncResponse(syncResponse, isInitialSync)
|
||||||
|
|
||||||
|
markCryptoSyncCompleted(syncResponse)
|
||||||
|
|
||||||
|
handlePostSync()
|
||||||
|
|
||||||
|
Timber.v("On sync completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCryptoService(isInitialSync: Boolean) {
|
||||||
|
relevantPlugins.measureSpan("task", "start_crypto_service") {
|
||||||
|
measureTimeMillis {
|
||||||
|
if (!cryptoService.isStarted()) {
|
||||||
|
Timber.v("Should start cryptoService")
|
||||||
|
cryptoService.start()
|
||||||
}
|
}
|
||||||
|
cryptoService.onSyncWillProcess(isInitialSync)
|
||||||
|
}.also {
|
||||||
|
Timber.v("Finish handling start cryptoService in $it ms")
|
||||||
}
|
}
|
||||||
}.also {
|
|
||||||
Timber.v("Finish handling toDevice in $it ms")
|
|
||||||
}
|
}
|
||||||
val aggregator = SyncResponsePostTreatmentAggregator()
|
}
|
||||||
|
|
||||||
// Prerequisite for thread events handling in RoomSyncHandler
|
private suspend fun handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) {
|
||||||
// Disabled due to the new fallback
|
relevantPlugins.measureSpan("task", "handle_to_device") {
|
||||||
// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
measureTimeMillis {
|
||||||
// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
|
Timber.v("Handle toDevice")
|
||||||
// }
|
reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) {
|
||||||
|
if (syncResponse.toDevice != null) {
|
||||||
|
cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
Timber.v("Finish handling toDevice in $it ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startMonarchyTransaction(
|
||||||
|
syncResponse: SyncResponse,
|
||||||
|
isInitialSync: Boolean,
|
||||||
|
reporter: ProgressReporter?,
|
||||||
|
aggregator: SyncResponsePostTreatmentAggregator
|
||||||
|
) {
|
||||||
// Start one big transaction
|
// Start one big transaction
|
||||||
monarchy.awaitTransaction { realm ->
|
relevantPlugins.measureSpan("task", "monarchy_transaction") {
|
||||||
// IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
|
monarchy.awaitTransaction { realm ->
|
||||||
|
// IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
|
||||||
|
handleRooms(reporter, syncResponse, realm, isInitialSync, aggregator)
|
||||||
|
handleAccountData(reporter, realm, syncResponse)
|
||||||
|
handlePresence(realm, syncResponse)
|
||||||
|
|
||||||
|
tokenStore.saveToken(realm, syncResponse.nextBatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRooms(
|
||||||
|
reporter: ProgressReporter?,
|
||||||
|
syncResponse: SyncResponse,
|
||||||
|
realm: Realm,
|
||||||
|
isInitialSync: Boolean,
|
||||||
|
aggregator: SyncResponsePostTreatmentAggregator
|
||||||
|
) {
|
||||||
|
relevantPlugins.measureSpan("task", "handle_rooms") {
|
||||||
measureTimeMillis {
|
measureTimeMillis {
|
||||||
Timber.v("Handle rooms")
|
Timber.v("Handle rooms")
|
||||||
reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) {
|
reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) {
|
||||||
|
@ -106,7 +166,11 @@ internal class SyncResponseHandler @Inject constructor(
|
||||||
}.also {
|
}.also {
|
||||||
Timber.v("Finish handling rooms in $it ms")
|
Timber.v("Finish handling rooms in $it ms")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAccountData(reporter: ProgressReporter?, realm: Realm, syncResponse: SyncResponse) {
|
||||||
|
relevantPlugins.measureSpan("task", "handle_account_data") {
|
||||||
measureTimeMillis {
|
measureTimeMillis {
|
||||||
reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) {
|
reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) {
|
||||||
Timber.v("Handle accountData")
|
Timber.v("Handle accountData")
|
||||||
|
@ -115,44 +179,59 @@ internal class SyncResponseHandler @Inject constructor(
|
||||||
}.also {
|
}.also {
|
||||||
Timber.v("Finish handling accountData in $it ms")
|
Timber.v("Finish handling accountData in $it ms")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePresence(realm: Realm, syncResponse: SyncResponse) {
|
||||||
|
relevantPlugins.measureSpan("task", "handle_presence") {
|
||||||
measureTimeMillis {
|
measureTimeMillis {
|
||||||
Timber.v("Handle Presence")
|
Timber.v("Handle Presence")
|
||||||
presenceSyncHandler.handle(realm, syncResponse.presence)
|
presenceSyncHandler.handle(realm, syncResponse.presence)
|
||||||
}.also {
|
}.also {
|
||||||
Timber.v("Finish handling Presence in $it ms")
|
Timber.v("Finish handling Presence in $it ms")
|
||||||
}
|
}
|
||||||
tokenStore.saveToken(realm, syncResponse.nextBatch)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Everything else we need to do outside the transaction
|
private suspend fun aggregateSyncResponse(aggregator: SyncResponsePostTreatmentAggregator) {
|
||||||
measureTimeMillis {
|
relevantPlugins.measureSpan("task", "aggregator_management") {
|
||||||
aggregatorHandler.handle(aggregator)
|
// Everything else we need to do outside the transaction
|
||||||
}.also {
|
measureTimeMillis {
|
||||||
Timber.v("Aggregator management took $it ms")
|
aggregatorHandler.handle(aggregator)
|
||||||
}
|
}.also {
|
||||||
|
Timber.v("Aggregator management took $it ms")
|
||||||
measureTimeMillis {
|
|
||||||
syncResponse.rooms?.let {
|
|
||||||
checkPushRules(it, isInitialSync)
|
|
||||||
userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
|
|
||||||
dispatchInvitedRoom(it)
|
|
||||||
}
|
}
|
||||||
}.also {
|
|
||||||
Timber.v("SyncResponse.rooms post treatment took $it ms")
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
measureTimeMillis {
|
private suspend fun postTreatmentSyncResponse(syncResponse: SyncResponse, isInitialSync: Boolean) {
|
||||||
cryptoSyncHandler.onSyncCompleted(syncResponse)
|
relevantPlugins.measureSpan("task", "sync_response_post_treatment") {
|
||||||
}.also {
|
measureTimeMillis {
|
||||||
Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
|
syncResponse.rooms?.let {
|
||||||
|
checkPushRules(it, isInitialSync)
|
||||||
|
userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
|
||||||
|
dispatchInvitedRoom(it)
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
Timber.v("SyncResponse.rooms post treatment took $it ms")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// post sync stuffs
|
private fun markCryptoSyncCompleted(syncResponse: SyncResponse) {
|
||||||
|
relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") {
|
||||||
|
measureTimeMillis {
|
||||||
|
cryptoSyncHandler.onSyncCompleted(syncResponse)
|
||||||
|
}.also {
|
||||||
|
Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePostSync() {
|
||||||
monarchy.writeAsync {
|
monarchy.writeAsync {
|
||||||
roomSyncHandler.postSyncSpaceHierarchyHandle(it)
|
roomSyncHandler.postSyncSpaceHierarchyHandle(it)
|
||||||
}
|
}
|
||||||
Timber.v("On sync completed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
|
private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.core.notification
|
||||||
|
|
||||||
|
import im.vector.app.features.session.coroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class EnableNotificationsSettingUpdater @Inject constructor(
|
||||||
|
private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
fun onSessionsStarted(session: Session) {
|
||||||
|
job?.cancel()
|
||||||
|
job = session.coroutineScope.launch {
|
||||||
|
updateEnableNotificationsSettingOnChangeUseCase.execute(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.core.notification
|
||||||
|
|
||||||
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for changes in either Pusher or Account data to update the local enable notifications
|
||||||
|
* setting for the current device.
|
||||||
|
*/
|
||||||
|
class UpdateEnableNotificationsSettingOnChangeUseCase @Inject constructor(
|
||||||
|
private val vectorPreferences: VectorPreferences,
|
||||||
|
private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun execute(session: Session) {
|
||||||
|
val deviceId = session.sessionParams.deviceId ?: return
|
||||||
|
getNotificationsStatusUseCase.execute(session, deviceId)
|
||||||
|
.onEach(::updatePreference)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePreference(notificationStatus: NotificationsStatus) {
|
||||||
|
when (notificationStatus) {
|
||||||
|
NotificationsStatus.ENABLED -> vectorPreferences.setNotificationEnabledForDevice(true)
|
||||||
|
NotificationsStatus.DISABLED -> vectorPreferences.setNotificationEnabledForDevice(false)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,12 +97,6 @@ class PushersManager @Inject constructor(
|
||||||
return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
|
return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun togglePusherForCurrentSession(enable: Boolean) {
|
|
||||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
|
||||||
val pusher = getPusherForCurrentSession() ?: return
|
|
||||||
session.pushersService().togglePusher(pusher, enable)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun unregisterEmailPusher(email: String) {
|
suspend fun unregisterEmailPusher(email: String) {
|
||||||
val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
|
val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
|
||||||
currentSession.pushersService().removeEmailPusher(email)
|
currentSession.pushersService().removeEmailPusher(email)
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.core.session
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import im.vector.app.core.extensions.startSyncing
|
import im.vector.app.core.extensions.startSyncing
|
||||||
|
import im.vector.app.core.notification.EnableNotificationsSettingUpdater
|
||||||
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
|
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
@ -32,6 +33,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
|
||||||
private val webRtcCallManager: WebRtcCallManager,
|
private val webRtcCallManager: WebRtcCallManager,
|
||||||
private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
|
private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
|
private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun execute(session: Session, startSyncing: Boolean = true) {
|
suspend fun execute(session: Session, startSyncing: Boolean = true) {
|
||||||
|
@ -46,5 +48,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
|
||||||
if (vectorPreferences.isClientInfoRecordingEnabled()) {
|
if (vectorPreferences.isClientInfoRecordingEnabled()) {
|
||||||
updateMatrixClientInfoUseCase.execute(session)
|
updateMatrixClientInfoUseCase.execute(session)
|
||||||
}
|
}
|
||||||
|
enableNotificationsSettingUpdater.onSessionsStarted(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.app.features.analytics.metrics
|
package im.vector.app.features.analytics.metrics
|
||||||
|
|
||||||
import im.vector.app.features.analytics.metrics.sentry.SentryDownloadDeviceKeysMetrics
|
import im.vector.app.features.analytics.metrics.sentry.SentryDownloadDeviceKeysMetrics
|
||||||
|
import im.vector.app.features.analytics.metrics.sentry.SentrySyncDurationMetrics
|
||||||
import org.matrix.android.sdk.api.metrics.MetricPlugin
|
import org.matrix.android.sdk.api.metrics.MetricPlugin
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -27,9 +28,10 @@ import javax.inject.Singleton
|
||||||
@Singleton
|
@Singleton
|
||||||
data class VectorPlugins @Inject constructor(
|
data class VectorPlugins @Inject constructor(
|
||||||
val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics,
|
val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics,
|
||||||
|
val sentrySyncDurationMetrics: SentrySyncDurationMetrics,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Returns [List] of all [MetricPlugin] hold by this class.
|
* Returns [List] of all [MetricPlugin] hold by this class.
|
||||||
*/
|
*/
|
||||||
fun plugins(): List<MetricPlugin> = listOf(sentryDownloadDeviceKeysMetrics)
|
fun plugins(): List<MetricPlugin> = listOf(sentryDownloadDeviceKeysMetrics, sentrySyncDurationMetrics)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,10 @@ class SentryDownloadDeviceKeysMetrics @Inject constructor() : DownloadDeviceKeys
|
||||||
private var transaction: ITransaction? = null
|
private var transaction: ITransaction? = null
|
||||||
|
|
||||||
override fun startTransaction() {
|
override fun startTransaction() {
|
||||||
transaction = Sentry.startTransaction("download_device_keys", "task")
|
if (Sentry.isEnabled()) {
|
||||||
logTransaction("Sentry transaction started")
|
transaction = Sentry.startTransaction("download_device_keys", "task")
|
||||||
|
logTransaction("Sentry transaction started")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finishTransaction() {
|
override fun finishTransaction() {
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.analytics.metrics.sentry
|
||||||
|
|
||||||
|
import io.sentry.ISpan
|
||||||
|
import io.sentry.ITransaction
|
||||||
|
import io.sentry.Sentry
|
||||||
|
import io.sentry.SpanStatus
|
||||||
|
import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin
|
||||||
|
import java.util.EmptyStackException
|
||||||
|
import java.util.Stack
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentry based implementation of SyncDurationMetricPlugin.
|
||||||
|
*/
|
||||||
|
class SentrySyncDurationMetrics @Inject constructor() : SyncDurationMetricPlugin {
|
||||||
|
private var transaction: ITransaction? = null
|
||||||
|
|
||||||
|
// Stacks to keep spans in LIFO order.
|
||||||
|
private var spans: Stack<ISpan> = Stack()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the span for a sub-task.
|
||||||
|
*
|
||||||
|
* @param operation Name of the new span.
|
||||||
|
* @param description Description of the new span.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if this is called without starting a transaction ie. `measureSpan` must be called within `measureMetric`.
|
||||||
|
*/
|
||||||
|
override fun startSpan(operation: String, description: String) {
|
||||||
|
if (Sentry.isEnabled()) {
|
||||||
|
val span = Sentry.getSpan() ?: throw IllegalStateException("measureSpan block must be called within measureMetric")
|
||||||
|
val innerSpan = span.startChild(operation, description)
|
||||||
|
spans.push(innerSpan)
|
||||||
|
logTransaction("Sentry span started: operation=[$operation], description=[$description]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishSpan() {
|
||||||
|
try {
|
||||||
|
spans.pop()
|
||||||
|
} catch (e: EmptyStackException) {
|
||||||
|
null
|
||||||
|
}?.finish()
|
||||||
|
logTransaction("Sentry span finished")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startTransaction() {
|
||||||
|
if (Sentry.isEnabled()) {
|
||||||
|
transaction = Sentry.startTransaction("sync_response_handler", "task", true)
|
||||||
|
logTransaction("Sentry transaction started")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishTransaction() {
|
||||||
|
transaction?.finish()
|
||||||
|
logTransaction("Sentry transaction finished")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(throwable: Throwable) {
|
||||||
|
try {
|
||||||
|
spans.peek()
|
||||||
|
} catch (e: EmptyStackException) {
|
||||||
|
null
|
||||||
|
}?.apply {
|
||||||
|
this.throwable = throwable
|
||||||
|
this.status = SpanStatus.INTERNAL_ERROR
|
||||||
|
} ?: transaction?.apply {
|
||||||
|
this.throwable = throwable
|
||||||
|
this.status = SpanStatus.INTERNAL_ERROR
|
||||||
|
}
|
||||||
|
logTransaction("Sentry transaction encountered error ${throwable.message}")
|
||||||
|
}
|
||||||
|
}
|
|
@ -167,12 +167,10 @@ class WebRtcCall(
|
||||||
private var screenSender: RtpSender? = null
|
private var screenSender: RtpSender? = null
|
||||||
|
|
||||||
private val timer = CountUpTimer(1000L).apply {
|
private val timer = CountUpTimer(1000L).apply {
|
||||||
tickListener = object : CountUpTimer.TickListener {
|
tickListener = CountUpTimer.TickListener { milliseconds ->
|
||||||
override fun onTick(milliseconds: Long) {
|
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
|
||||||
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
|
listeners.forEach {
|
||||||
listeners.forEach {
|
tryOrNull { it.onTick(formattedDuration) }
|
||||||
tryOrNull { it.onTick(formattedDuration) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.net.Uri
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.call.conference.ConferenceEvent
|
import im.vector.app.features.call.conference.ConferenceEvent
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||||
|
@ -129,10 +130,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Listening : VoiceBroadcastAction() {
|
sealed class Listening : VoiceBroadcastAction() {
|
||||||
data class PlayOrResume(val voiceBroadcastId: String) : Listening()
|
data class PlayOrResume(val voiceBroadcast: VoiceBroadcast) : Listening()
|
||||||
object Pause : Listening()
|
object Pause : Listening()
|
||||||
object Stop : Listening()
|
object Stop : Listening()
|
||||||
data class SeekTo(val voiceBroadcastId: String, val positionMillis: Int) : Listening()
|
data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int, val duration: Int) : Listening()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -634,10 +634,10 @@ class TimelineViewModel @AssistedInject constructor(
|
||||||
VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
|
VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
|
||||||
VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
|
VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
|
||||||
VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
||||||
is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.voiceBroadcastId)
|
is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast)
|
||||||
VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
|
VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
|
||||||
VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
|
VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
|
||||||
is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcastId, action.positionMillis)
|
is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis, action.duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,11 +199,7 @@ class AudioMessageHelper @Inject constructor(
|
||||||
private fun startRecordingAmplitudes() {
|
private fun startRecordingAmplitudes() {
|
||||||
amplitudeTicker?.stop()
|
amplitudeTicker?.stop()
|
||||||
amplitudeTicker = CountUpTimer(50).apply {
|
amplitudeTicker = CountUpTimer(50).apply {
|
||||||
tickListener = object : CountUpTimer.TickListener {
|
tickListener = CountUpTimer.TickListener { onAmplitudeTick() }
|
||||||
override fun onTick(milliseconds: Long) {
|
|
||||||
onAmplitudeTick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resume()
|
resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,11 +230,7 @@ class AudioMessageHelper @Inject constructor(
|
||||||
private fun startPlaybackTicker(id: String) {
|
private fun startPlaybackTicker(id: String) {
|
||||||
playbackTicker?.stop()
|
playbackTicker?.stop()
|
||||||
playbackTicker = CountUpTimer().apply {
|
playbackTicker = CountUpTimer().apply {
|
||||||
tickListener = object : CountUpTimer.TickListener {
|
tickListener = CountUpTimer.TickListener { onPlaybackTick(id) }
|
||||||
override fun onTick(milliseconds: Long) {
|
|
||||||
onPlaybackTick(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resume()
|
resume()
|
||||||
}
|
}
|
||||||
onPlaybackTick(id)
|
onPlaybackTick(id)
|
||||||
|
|
|
@ -189,11 +189,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||||
val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0)
|
val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0)
|
||||||
recordingTicker?.stop()
|
recordingTicker?.stop()
|
||||||
recordingTicker = CountUpTimer().apply {
|
recordingTicker = CountUpTimer().apply {
|
||||||
tickListener = object : CountUpTimer.TickListener {
|
tickListener = CountUpTimer.TickListener { milliseconds ->
|
||||||
override fun onTick(milliseconds: Long) {
|
val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
|
||||||
val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
|
onRecordingTick(isLocked, milliseconds + startMs)
|
||||||
onRecordingTick(isLocked, milliseconds + startMs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
resume()
|
resume()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
import im.vector.app.core.resources.DrawableProvider
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||||
|
@ -28,6 +29,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadca
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
|
@ -44,6 +46,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
||||||
private val drawableProvider: DrawableProvider,
|
private val drawableProvider: DrawableProvider,
|
||||||
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||||
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
|
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
|
||||||
|
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
|
@ -58,19 +61,20 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
||||||
val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
||||||
val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
|
val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
|
||||||
val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null
|
val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null
|
||||||
val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId
|
val voiceBroadcast = VoiceBroadcast(voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId, roomId = params.event.roomId)
|
||||||
|
|
||||||
val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED &&
|
val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED &&
|
||||||
voiceBroadcastEvent.root.stateKey == session.myUserId &&
|
voiceBroadcastEvent.root.stateKey == session.myUserId &&
|
||||||
messageContent.deviceId == session.sessionParams.deviceId
|
messageContent.deviceId == session.sessionParams.deviceId
|
||||||
|
|
||||||
val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
|
val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
|
||||||
voiceBroadcastId = voiceBroadcastId,
|
voiceBroadcast = voiceBroadcast,
|
||||||
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
||||||
duration = voiceBroadcastEventsGroup.getDuration(),
|
duration = voiceBroadcastEventsGroup.getDuration(),
|
||||||
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
|
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
|
||||||
recorder = voiceBroadcastRecorder,
|
recorder = voiceBroadcastRecorder,
|
||||||
player = voiceBroadcastPlayer,
|
player = voiceBroadcastPlayer,
|
||||||
|
playbackTracker = playbackTracker,
|
||||||
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
||||||
colorProvider = colorProvider,
|
colorProvider = colorProvider,
|
||||||
drawableProvider = drawableProvider,
|
drawableProvider = drawableProvider,
|
||||||
|
@ -89,7 +93,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
||||||
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||||
): MessageVoiceBroadcastRecordingItem {
|
): MessageVoiceBroadcastRecordingItem {
|
||||||
return MessageVoiceBroadcastRecordingItem_()
|
return MessageVoiceBroadcastRecordingItem_()
|
||||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}")
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
|
@ -102,7 +106,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
||||||
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||||
): MessageVoiceBroadcastListeningItem {
|
): MessageVoiceBroadcastListeningItem {
|
||||||
return MessageVoiceBroadcastListeningItem_()
|
return MessageVoiceBroadcastListeningItem_()
|
||||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}")
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
|
|
|
@ -127,7 +127,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPercentage(id: String): Float {
|
fun getPercentage(id: String): Float {
|
||||||
return when (val state = states[id]) {
|
return when (val state = states[id]) {
|
||||||
is Listener.State.Playing -> state.percentage
|
is Listener.State.Playing -> state.percentage
|
||||||
is Listener.State.Paused -> state.percentage
|
is Listener.State.Paused -> state.percentage
|
||||||
|
@ -148,7 +148,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
const val RECORDING_ID = "RECORDING_ID"
|
const val RECORDING_ID = "RECORDING_ID"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Listener {
|
fun interface Listener {
|
||||||
|
|
||||||
fun onUpdate(state: State)
|
fun onUpdate(state: State)
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,9 @@ import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.tintBackground
|
import im.vector.app.core.extensions.tintBackground
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
import im.vector.app.core.resources.DrawableProvider
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
@ -35,11 +37,13 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var voiceBroadcastAttributes: Attributes
|
lateinit var voiceBroadcastAttributes: Attributes
|
||||||
|
|
||||||
protected val voiceBroadcastId get() = voiceBroadcastAttributes.voiceBroadcastId
|
protected val voiceBroadcast get() = voiceBroadcastAttributes.voiceBroadcast
|
||||||
protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState
|
protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState
|
||||||
protected val recorderName get() = voiceBroadcastAttributes.recorderName
|
protected val recorderName get() = voiceBroadcastAttributes.recorderName
|
||||||
protected val recorder get() = voiceBroadcastAttributes.recorder
|
protected val recorder get() = voiceBroadcastAttributes.recorder
|
||||||
protected val player get() = voiceBroadcastAttributes.player
|
protected val player get() = voiceBroadcastAttributes.player
|
||||||
|
protected val playbackTracker get() = voiceBroadcastAttributes.playbackTracker
|
||||||
|
protected val duration get() = voiceBroadcastAttributes.duration
|
||||||
protected val roomItem get() = voiceBroadcastAttributes.roomItem
|
protected val roomItem get() = voiceBroadcastAttributes.roomItem
|
||||||
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
|
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
|
||||||
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
||||||
|
@ -92,12 +96,13 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Attributes(
|
data class Attributes(
|
||||||
val voiceBroadcastId: String,
|
val voiceBroadcast: VoiceBroadcast,
|
||||||
val voiceBroadcastState: VoiceBroadcastState?,
|
val voiceBroadcastState: VoiceBroadcastState?,
|
||||||
val duration: Int,
|
val duration: Int,
|
||||||
val recorderName: String,
|
val recorderName: String,
|
||||||
val recorder: VoiceBroadcastRecorder?,
|
val recorder: VoiceBroadcastRecorder?,
|
||||||
val player: VoiceBroadcastPlayer,
|
val player: VoiceBroadcastPlayer,
|
||||||
|
val playbackTracker: AudioMessagePlaybackTracker,
|
||||||
val roomItem: MatrixItem?,
|
val roomItem: MatrixItem?,
|
||||||
val colorProvider: ColorProvider,
|
val colorProvider: ColorProvider,
|
||||||
val drawableProvider: DrawableProvider,
|
val drawableProvider: DrawableProvider,
|
||||||
|
|
|
@ -140,16 +140,14 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
||||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
when (state) {
|
||||||
when (state) {
|
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderIdleState(holder: Holder) {
|
private fun renderIdleState(holder: Holder) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||||
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastListeningItem.Holder>() {
|
abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastListeningItem.Holder>() {
|
||||||
|
|
||||||
private lateinit var playerListener: VoiceBroadcastPlayer.Listener
|
private lateinit var playerListener: VoiceBroadcastPlayer.Listener
|
||||||
|
private var isUserSeeking = false
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
|
@ -41,11 +43,35 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||||
playerListener = VoiceBroadcastPlayer.Listener { state ->
|
playerListener = VoiceBroadcastPlayer.Listener { renderPlayingState(holder, it) }
|
||||||
renderPlayingState(holder, state)
|
player.addListener(voiceBroadcast, playerListener)
|
||||||
}
|
|
||||||
player.addListener(voiceBroadcastId, playerListener)
|
|
||||||
bindSeekBar(holder)
|
bindSeekBar(holder)
|
||||||
|
bindButtons(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindButtons(holder: Holder) {
|
||||||
|
with(holder) {
|
||||||
|
playPauseButton.onClick {
|
||||||
|
if (player.currentVoiceBroadcast == voiceBroadcast) {
|
||||||
|
when (player.playingState) {
|
||||||
|
VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
|
||||||
|
VoiceBroadcastPlayer.State.PAUSED,
|
||||||
|
VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||||
|
VoiceBroadcastPlayer.State.BUFFERING -> Unit
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fastBackwardButton.onClick {
|
||||||
|
val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration)
|
||||||
|
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
|
||||||
|
}
|
||||||
|
fastForwardButton.onClick {
|
||||||
|
val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration)
|
||||||
|
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun renderMetadata(holder: Holder) {
|
override fun renderMetadata(holder: Holder) {
|
||||||
|
@ -61,50 +87,67 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
||||||
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
||||||
|
|
||||||
fastBackwardButton.isInvisible = true
|
|
||||||
fastForwardButton.isInvisible = true
|
|
||||||
|
|
||||||
when (state) {
|
when (state) {
|
||||||
VoiceBroadcastPlayer.State.PLAYING -> {
|
VoiceBroadcastPlayer.State.PLAYING -> {
|
||||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
||||||
playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) }
|
|
||||||
seekBar.isEnabled = true
|
|
||||||
}
|
}
|
||||||
VoiceBroadcastPlayer.State.IDLE,
|
VoiceBroadcastPlayer.State.IDLE,
|
||||||
VoiceBroadcastPlayer.State.PAUSED -> {
|
VoiceBroadcastPlayer.State.PAUSED -> {
|
||||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
||||||
playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) }
|
|
||||||
seekBar.isEnabled = false
|
|
||||||
}
|
|
||||||
VoiceBroadcastPlayer.State.BUFFERING -> {
|
|
||||||
seekBar.isEnabled = true
|
|
||||||
}
|
}
|
||||||
|
VoiceBroadcastPlayer.State.BUFFERING -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindSeekBar(holder: Holder) {
|
private fun bindSeekBar(holder: Holder) {
|
||||||
holder.durationView.text = formatPlaybackTime(voiceBroadcastAttributes.duration)
|
with(holder) {
|
||||||
holder.seekBar.max = voiceBroadcastAttributes.duration
|
durationView.text = formatPlaybackTime(duration)
|
||||||
holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
seekBar.max = duration
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit
|
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) = Unit
|
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||||
|
isUserSeeking = true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress))
|
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration))
|
||||||
|
isUserSeeking = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
|
||||||
|
renderBackwardForwardButtons(holder, playbackState)
|
||||||
|
if (!isUserSeeking) {
|
||||||
|
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
|
||||||
|
val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused
|
||||||
|
val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
|
||||||
|
val canBackward = isPlayingOrPaused && playbackTime > 0
|
||||||
|
val canForward = isPlayingOrPaused && playbackTime < duration
|
||||||
|
holder.fastBackwardButton.isInvisible = !canBackward
|
||||||
|
holder.fastForwardButton.isInvisible = !canForward
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
|
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
super.unbind(holder)
|
super.unbind(holder)
|
||||||
player.removeListener(voiceBroadcastId, playerListener)
|
player.removeListener(voiceBroadcast, playerListener)
|
||||||
holder.seekBar.setOnSeekBarChangeListener(null)
|
playbackTracker.untrack(voiceBroadcast.voiceBroadcastId)
|
||||||
|
with(holder) {
|
||||||
|
seekBar.onClick(null)
|
||||||
|
playPauseButton.onClick(null)
|
||||||
|
fastForwardButton.onClick(null)
|
||||||
|
fastBackwardButton.onClick(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
|
@ -122,16 +122,14 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
when (state) {
|
||||||
when (state) {
|
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
|
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
|
private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
|
||||||
|
|
|
@ -79,10 +79,8 @@ abstract class LiveLocationUserItem : VectorEpoxyModel<LiveLocationUserItem.Hold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.timer.tickListener = object : CountUpTimer.TickListener {
|
holder.timer.tickListener = CountUpTimer.TickListener {
|
||||||
override fun onTick(milliseconds: Long) {
|
holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
|
||||||
holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
holder.timer.resume()
|
holder.timer.resume()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.settings.devices.v2.notification
|
||||||
|
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.flow.unwrap
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() {
|
||||||
|
|
||||||
|
fun execute(session: Session): Flow<Boolean> {
|
||||||
|
return session
|
||||||
|
.homeServerCapabilitiesService()
|
||||||
|
.getHomeServerCapabilitiesLive()
|
||||||
|
.asFlow()
|
||||||
|
.unwrap()
|
||||||
|
.map { it.canRemotelyTogglePushNotificationsOfDevices }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,18 +16,15 @@
|
||||||
|
|
||||||
package im.vector.app.features.settings.devices.v2.notification
|
package im.vector.app.features.settings.devices.v2.notification
|
||||||
|
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
|
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor(
|
class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() {
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun execute(deviceId: String): Boolean {
|
fun execute(session: Session, deviceId: String): Boolean {
|
||||||
return activeSessionHolder
|
return session
|
||||||
.getSafeActiveSession()
|
.accountDataService()
|
||||||
?.accountDataService()
|
.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null
|
||||||
?.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,20 +16,15 @@
|
||||||
|
|
||||||
package im.vector.app.features.settings.devices.v2.notification
|
package im.vector.app.features.settings.devices.v2.notification
|
||||||
|
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor(
|
class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() {
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun execute(): Boolean {
|
fun execute(session: Session): Boolean {
|
||||||
return activeSessionHolder
|
return session
|
||||||
.getSafeActiveSession()
|
.homeServerCapabilitiesService()
|
||||||
?.homeServerCapabilitiesService()
|
.getHomeServerCapabilities()
|
||||||
?.getHomeServerCapabilities()
|
.canRemotelyTogglePushNotificationsOfDevices
|
||||||
?.canRemotelyTogglePushNotificationsOfDevices
|
|
||||||
.orFalse()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,13 @@
|
||||||
|
|
||||||
package im.vector.app.features.settings.devices.v2.notification
|
package im.vector.app.features.settings.devices.v2.notification
|
||||||
|
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
|
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
|
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.flow.flow
|
import org.matrix.android.sdk.flow.flow
|
||||||
|
@ -29,16 +30,13 @@ import org.matrix.android.sdk.flow.unwrap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class GetNotificationsStatusUseCase @Inject constructor(
|
class GetNotificationsStatusUseCase @Inject constructor(
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase,
|
||||||
private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
|
|
||||||
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
|
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun execute(deviceId: String): Flow<NotificationsStatus> {
|
fun execute(session: Session, deviceId: String): Flow<NotificationsStatus> {
|
||||||
val session = activeSessionHolder.getSafeActiveSession()
|
|
||||||
return when {
|
return when {
|
||||||
session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED)
|
checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> {
|
||||||
checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> {
|
|
||||||
session.flow()
|
session.flow()
|
||||||
.liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
|
.liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -46,15 +44,19 @@ class GetNotificationsStatusUseCase @Inject constructor(
|
||||||
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
|
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> {
|
else -> canTogglePushNotificationsViaPusherUseCase.execute(session)
|
||||||
session.flow()
|
.flatMapLatest { canToggle ->
|
||||||
.livePushers()
|
if (canToggle) {
|
||||||
.map { it.filter { pusher -> pusher.deviceId == deviceId } }
|
session.flow()
|
||||||
.map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
|
.livePushers()
|
||||||
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
|
.map { it.filter { pusher -> pusher.deviceId == deviceId } }
|
||||||
.distinctUntilChanged()
|
.map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
|
||||||
}
|
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
|
||||||
else -> flowOf(NotificationsStatus.NOT_SUPPORTED)
|
.distinctUntilChanged()
|
||||||
|
} else {
|
||||||
|
flowOf(NotificationsStatus.NOT_SUPPORTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,14 +31,14 @@ class TogglePushNotificationUseCase @Inject constructor(
|
||||||
suspend fun execute(deviceId: String, enabled: Boolean) {
|
suspend fun execute(deviceId: String, enabled: Boolean) {
|
||||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||||
|
|
||||||
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) {
|
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
|
||||||
val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
|
val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
|
||||||
devicePusher?.let { pusher ->
|
devicePusher?.let { pusher ->
|
||||||
session.pushersService().togglePusher(pusher, enabled)
|
session.pushersService().togglePusher(pusher, enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId)) {
|
if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) {
|
||||||
val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
|
val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
|
||||||
session.accountDataService().updateUserAccountData(
|
session.accountDataService().updateUserAccountData(
|
||||||
UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId,
|
UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId,
|
||||||
|
|
|
@ -267,7 +267,10 @@ class OtherSessionsFragment :
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found)
|
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found)
|
||||||
updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_verified_title, R.string.device_manager_learn_more_sessions_verified)
|
updateSecurityLearnMoreButton(
|
||||||
|
R.string.device_manager_learn_more_sessions_verified_title,
|
||||||
|
R.string.device_manager_learn_more_sessions_verified_description
|
||||||
|
)
|
||||||
}
|
}
|
||||||
DeviceManagerFilterType.UNVERIFIED -> {
|
DeviceManagerFilterType.UNVERIFIED -> {
|
||||||
views.otherSessionsSecurityRecommendationView.render(
|
views.otherSessionsSecurityRecommendationView.render(
|
||||||
|
|
|
@ -300,7 +300,7 @@ class SessionOverviewFragment :
|
||||||
R.string.device_manager_verification_status_unverified
|
R.string.device_manager_verification_status_unverified
|
||||||
}
|
}
|
||||||
val descriptionResId = if (isVerified) {
|
val descriptionResId = if (isVerified) {
|
||||||
R.string.device_manager_learn_more_sessions_verified
|
R.string.device_manager_learn_more_sessions_verified_description
|
||||||
} else {
|
} else {
|
||||||
R.string.device_manager_learn_more_sessions_unverified
|
R.string.device_manager_learn_more_sessions_unverified
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,9 +110,11 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeNotificationsStatus(deviceId: String) {
|
private fun observeNotificationsStatus(deviceId: String) {
|
||||||
getNotificationsStatusUseCase.execute(deviceId)
|
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||||
.onEach { setState { copy(notificationsStatus = it) } }
|
getNotificationsStatusUseCase.execute(session, deviceId)
|
||||||
.launchIn(viewModelScope)
|
.onEach { setState { copy(notificationsStatus = it) } }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: SessionOverviewAction) {
|
override fun handle(action: SessionOverviewAction) {
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.settings.notifications
|
||||||
|
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.core.pushers.PushersManager
|
||||||
|
import im.vector.app.core.pushers.UnifiedPushHelper
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
private val unifiedPushHelper: UnifiedPushHelper,
|
||||||
|
private val pushersManager: PushersManager,
|
||||||
|
private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
|
||||||
|
private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun execute() {
|
||||||
|
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||||
|
val deviceId = session.sessionParams.deviceId ?: return
|
||||||
|
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
|
||||||
|
togglePushNotificationUseCase.execute(deviceId, enabled = false)
|
||||||
|
} else {
|
||||||
|
unifiedPushHelper.unregister(pushersManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.settings.notifications
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.core.pushers.FcmHelper
|
||||||
|
import im.vector.app.core.pushers.PushersManager
|
||||||
|
import im.vector.app.core.pushers.UnifiedPushHelper
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
private val unifiedPushHelper: UnifiedPushHelper,
|
||||||
|
private val pushersManager: PushersManager,
|
||||||
|
private val fcmHelper: FcmHelper,
|
||||||
|
private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
|
||||||
|
private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun execute(fragmentActivity: FragmentActivity) {
|
||||||
|
val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
|
||||||
|
if (pusherForCurrentSession == null) {
|
||||||
|
registerPusher(fragmentActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||||
|
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
|
||||||
|
val deviceId = session.sessionParams.deviceId ?: return
|
||||||
|
togglePushNotificationUseCase.execute(deviceId, enabled = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun registerPusher(fragmentActivity: FragmentActivity) {
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
try {
|
||||||
|
unifiedPushHelper.register(fragmentActivity) {
|
||||||
|
if (unifiedPushHelper.isEmbeddedDistributor()) {
|
||||||
|
fcmHelper.ensureFcmTokenIsRetrieved(
|
||||||
|
fragmentActivity,
|
||||||
|
pushersManager,
|
||||||
|
registerPusher = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
continuation.resume(Unit)
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
continuation.resumeWithException(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,7 +57,6 @@ import im.vector.app.features.settings.VectorSettingsBaseFragment
|
||||||
import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener
|
import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener
|
||||||
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
|
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
@ -81,6 +80,8 @@ class VectorSettingsNotificationPreferenceFragment :
|
||||||
@Inject lateinit var guardServiceStarter: GuardServiceStarter
|
@Inject lateinit var guardServiceStarter: GuardServiceStarter
|
||||||
@Inject lateinit var vectorFeatures: VectorFeatures
|
@Inject lateinit var vectorFeatures: VectorFeatures
|
||||||
@Inject lateinit var notificationPermissionManager: NotificationPermissionManager
|
@Inject lateinit var notificationPermissionManager: NotificationPermissionManager
|
||||||
|
@Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase
|
||||||
|
@Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase
|
||||||
|
|
||||||
override var titleRes: Int = R.string.settings_notifications
|
override var titleRes: Int = R.string.settings_notifications
|
||||||
override val preferenceXmlRes = R.xml.vector_settings_notifications
|
override val preferenceXmlRes = R.xml.vector_settings_notifications
|
||||||
|
@ -119,48 +120,25 @@ class VectorSettingsNotificationPreferenceFragment :
|
||||||
(pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel
|
(pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let {
|
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
|
||||||
pushersManager.getPusherForCurrentSession()?.let { pusher ->
|
?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
|
||||||
it.isChecked = pusher.enabled
|
if (isChecked) {
|
||||||
}
|
enableNotificationsForCurrentSessionUseCase.execute(requireActivity())
|
||||||
|
|
||||||
it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
|
|
||||||
if (isChecked) {
|
|
||||||
unifiedPushHelper.register(requireActivity()) {
|
|
||||||
// Update the summary
|
|
||||||
if (unifiedPushHelper.isEmbeddedDistributor()) {
|
|
||||||
fcmHelper.ensureFcmTokenIsRetrieved(
|
|
||||||
requireActivity(),
|
|
||||||
pushersManager,
|
|
||||||
vectorPreferences.areNotificationEnabledForDevice()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)
|
findPreference<VectorPreference>(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)
|
||||||
?.summary = unifiedPushHelper.getCurrentDistributorName()
|
?.summary = unifiedPushHelper.getCurrentDistributorName()
|
||||||
lifecycleScope.launch {
|
|
||||||
val result = runCatching {
|
|
||||||
pushersManager.togglePusherForCurrentSession(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.exceptionOrNull()?.let { _ ->
|
notificationPermissionManager.eventuallyRequestPermission(
|
||||||
Toast.makeText(context, R.string.error_check_network, Toast.LENGTH_SHORT).show()
|
requireActivity(),
|
||||||
it.isChecked = false
|
postPermissionLauncher,
|
||||||
}
|
showRationale = false,
|
||||||
}
|
ignorePreference = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
disableNotificationsForCurrentSessionUseCase.execute()
|
||||||
|
notificationPermissionManager.eventuallyRevokePermission(requireActivity())
|
||||||
}
|
}
|
||||||
notificationPermissionManager.eventuallyRequestPermission(
|
|
||||||
requireActivity(),
|
|
||||||
postPermissionLauncher,
|
|
||||||
showRationale = false,
|
|
||||||
ignorePreference = true
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
unifiedPushHelper.unregister(pushersManager)
|
|
||||||
session.pushersService().refreshPushers()
|
|
||||||
notificationPermissionManager.eventuallyRevokePermission(requireActivity())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let {
|
findPreference<VectorPreference>(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let {
|
||||||
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
|
|
|
@ -16,7 +16,11 @@
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast
|
package im.vector.app.features.voicebroadcast
|
||||||
|
|
||||||
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
@ -34,3 +38,9 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? {
|
||||||
val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence
|
val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence
|
||||||
|
|
||||||
val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0
|
val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0
|
||||||
|
|
||||||
|
val VoiceBroadcastEvent.isLive
|
||||||
|
get() = content?.isLive.orFalse()
|
||||||
|
|
||||||
|
val MessageVoiceBroadcastInfoContent.isLive
|
||||||
|
get() = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.app.features.voicebroadcast
|
package im.vector.app.features.voicebroadcast
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
|
||||||
import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
|
||||||
import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
|
||||||
|
@ -41,15 +42,13 @@ class VoiceBroadcastHelper @Inject constructor(
|
||||||
|
|
||||||
suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
|
suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
|
||||||
|
|
||||||
fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId)
|
fun playOrResumePlayback(voiceBroadcast: VoiceBroadcast) = voiceBroadcastPlayer.playOrResume(voiceBroadcast)
|
||||||
|
|
||||||
fun pausePlayback() = voiceBroadcastPlayer.pause()
|
fun pausePlayback() = voiceBroadcastPlayer.pause()
|
||||||
|
|
||||||
fun stopPlayback() = voiceBroadcastPlayer.stop()
|
fun stopPlayback() = voiceBroadcastPlayer.stop()
|
||||||
|
|
||||||
fun seekTo(voiceBroadcastId: String, positionMillis: Int) {
|
fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) {
|
||||||
if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) {
|
voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis, duration)
|
||||||
voiceBroadcastPlayer.seekTo(positionMillis)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,14 @@
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast.listening
|
package im.vector.app.features.voicebroadcast.listening
|
||||||
|
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
|
|
||||||
interface VoiceBroadcastPlayer {
|
interface VoiceBroadcastPlayer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current playing voice broadcast identifier, if any.
|
* The current playing voice broadcast, if any.
|
||||||
*/
|
*/
|
||||||
val currentVoiceBroadcastId: String?
|
val currentVoiceBroadcast: VoiceBroadcast?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current playing [State], [State.IDLE] by default.
|
* The current playing [State], [State.IDLE] by default.
|
||||||
|
@ -31,7 +33,7 @@ interface VoiceBroadcastPlayer {
|
||||||
/**
|
/**
|
||||||
* Start playback of the given voice broadcast.
|
* Start playback of the given voice broadcast.
|
||||||
*/
|
*/
|
||||||
fun playOrResume(roomId: String, voiceBroadcastId: String)
|
fun playOrResume(voiceBroadcast: VoiceBroadcast)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pause playback of the current voice broadcast, if any.
|
* Pause playback of the current voice broadcast, if any.
|
||||||
|
@ -44,19 +46,19 @@ interface VoiceBroadcastPlayer {
|
||||||
fun stop()
|
fun stop()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seek to the given playback position, is milliseconds.
|
* Seek the given voice broadcast playback to the given position, is milliseconds.
|
||||||
*/
|
*/
|
||||||
fun seekTo(positionMillis: Int)
|
fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a [Listener] to the given voice broadcast id.
|
* Add a [Listener] to the given voice broadcast.
|
||||||
*/
|
*/
|
||||||
fun addListener(voiceBroadcastId: String, listener: Listener)
|
fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a [Listener] from the given voice broadcast id.
|
* Remove a [Listener] from the given voice broadcast.
|
||||||
*/
|
*/
|
||||||
fun removeListener(voiceBroadcastId: String, listener: Listener)
|
fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player states.
|
* Player states.
|
||||||
|
|
|
@ -18,28 +18,28 @@ package im.vector.app.features.voicebroadcast.listening
|
||||||
|
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
|
import android.media.MediaPlayer.OnPreparedListener
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.voice.VoiceFailure
|
import im.vector.app.features.voice.VoiceFailure
|
||||||
import im.vector.app.features.voicebroadcast.duration
|
import im.vector.app.features.voicebroadcast.isLive
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
|
||||||
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
|
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||||
import im.vector.app.features.voicebroadcast.sequence
|
import im.vector.app.features.voicebroadcast.sequence
|
||||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -49,179 +49,161 @@ import javax.inject.Singleton
|
||||||
class VoiceBroadcastPlayerImpl @Inject constructor(
|
class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
private val sessionHolder: ActiveSessionHolder,
|
private val sessionHolder: ActiveSessionHolder,
|
||||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||||
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
|
||||||
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
|
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
|
||||||
) : VoiceBroadcastPlayer {
|
) : VoiceBroadcastPlayer {
|
||||||
|
|
||||||
private val session
|
private val session get() = sessionHolder.getActiveSession()
|
||||||
get() = sessionHolder.getActiveSession()
|
private val sessionScope get() = session.coroutineScope
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
||||||
private var voiceBroadcastStateJob: Job? = null
|
|
||||||
|
|
||||||
private val mediaPlayerListener = MediaPlayerListener()
|
private val mediaPlayerListener = MediaPlayerListener()
|
||||||
|
private val playbackTicker = PlaybackTicker()
|
||||||
|
private val playlist = VoiceBroadcastPlaylist()
|
||||||
|
|
||||||
|
private var fetchPlaylistTask: Job? = null
|
||||||
|
private var voiceBroadcastStateObserver: Job? = null
|
||||||
|
|
||||||
private var currentMediaPlayer: MediaPlayer? = null
|
private var currentMediaPlayer: MediaPlayer? = null
|
||||||
private var nextMediaPlayer: MediaPlayer? = null
|
private var nextMediaPlayer: MediaPlayer? = null
|
||||||
private var currentSequence: Int? = null
|
private var isPreparingNextPlayer: Boolean = false
|
||||||
|
|
||||||
private var fetchPlaylistJob: Job? = null
|
private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
|
||||||
private var playlist = emptyList<PlaylistItem>()
|
|
||||||
|
|
||||||
private var isLive: Boolean = false
|
override var currentVoiceBroadcast: VoiceBroadcast? = null
|
||||||
|
|
||||||
override var currentVoiceBroadcastId: String? = null
|
|
||||||
|
|
||||||
override var playingState = State.IDLE
|
override var playingState = State.IDLE
|
||||||
@MainThread
|
@MainThread
|
||||||
set(value) {
|
set(value) {
|
||||||
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
if (field != value) {
|
||||||
field = value
|
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
||||||
// Notify state change to all the listeners attached to the current voice broadcast id
|
field = value
|
||||||
currentVoiceBroadcastId?.let { voiceBroadcastId ->
|
onPlayingStateChanged(value)
|
||||||
listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var currentRoomId: String? = null
|
|
||||||
|
|
||||||
/**
|
/** Map voiceBroadcastId to listeners.*/
|
||||||
* Map voiceBroadcastId to listeners.
|
|
||||||
*/
|
|
||||||
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
|
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
|
||||||
|
|
||||||
override fun playOrResume(roomId: String, voiceBroadcastId: String) {
|
override fun playOrResume(voiceBroadcast: VoiceBroadcast) {
|
||||||
val hasChanged = currentVoiceBroadcastId != voiceBroadcastId
|
val hasChanged = currentVoiceBroadcast != voiceBroadcast
|
||||||
when {
|
when {
|
||||||
hasChanged -> startPlayback(roomId, voiceBroadcastId)
|
hasChanged -> startPlayback(voiceBroadcast)
|
||||||
playingState == State.PAUSED -> resumePlayback()
|
playingState == State.PAUSED -> resumePlayback()
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pause() {
|
override fun pause() {
|
||||||
currentMediaPlayer?.pause()
|
pausePlayback()
|
||||||
currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
|
|
||||||
playingState = State.PAUSED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
// Stop playback
|
|
||||||
currentMediaPlayer?.stop()
|
|
||||||
currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
|
|
||||||
isLive = false
|
|
||||||
|
|
||||||
// Release current player
|
|
||||||
release(currentMediaPlayer)
|
|
||||||
currentMediaPlayer = null
|
|
||||||
|
|
||||||
// Release next player
|
|
||||||
release(nextMediaPlayer)
|
|
||||||
nextMediaPlayer = null
|
|
||||||
|
|
||||||
// Do not observe anymore voice broadcast state changes
|
|
||||||
voiceBroadcastStateJob?.cancel()
|
|
||||||
voiceBroadcastStateJob = null
|
|
||||||
|
|
||||||
// Do not fetch the playlist anymore
|
|
||||||
fetchPlaylistJob?.cancel()
|
|
||||||
fetchPlaylistJob = null
|
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
playingState = State.IDLE
|
playingState = State.IDLE
|
||||||
|
|
||||||
|
// Stop and release media players
|
||||||
|
stopPlayer()
|
||||||
|
|
||||||
|
// Do not observe anymore voice broadcast changes
|
||||||
|
fetchPlaylistTask?.cancel()
|
||||||
|
fetchPlaylistTask = null
|
||||||
|
voiceBroadcastStateObserver?.cancel()
|
||||||
|
voiceBroadcastStateObserver = null
|
||||||
|
|
||||||
// Clear playlist
|
// Clear playlist
|
||||||
playlist = emptyList()
|
playlist.reset()
|
||||||
currentSequence = null
|
|
||||||
|
|
||||||
currentRoomId = null
|
currentVoiceBroadcastEvent = null
|
||||||
currentVoiceBroadcastId = null
|
currentVoiceBroadcast = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addListener(voiceBroadcastId: String, listener: Listener) {
|
override fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
|
||||||
listeners[voiceBroadcastId]?.add(listener) ?: run {
|
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
|
||||||
listeners[voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||||
}
|
}
|
||||||
if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE)
|
listener.onStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeListener(voiceBroadcastId: String, listener: Listener) {
|
override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
|
||||||
listeners[voiceBroadcastId]?.remove(listener)
|
listeners[voiceBroadcast.voiceBroadcastId]?.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlayback(roomId: String, eventId: String) {
|
private fun startPlayback(voiceBroadcast: VoiceBroadcast) {
|
||||||
// Stop listening previous voice broadcast if any
|
// Stop listening previous voice broadcast if any
|
||||||
if (playingState != State.IDLE) stop()
|
if (playingState != State.IDLE) stop()
|
||||||
|
|
||||||
currentRoomId = roomId
|
currentVoiceBroadcast = voiceBroadcast
|
||||||
currentVoiceBroadcastId = eventId
|
|
||||||
|
|
||||||
playingState = State.BUFFERING
|
playingState = State.BUFFERING
|
||||||
|
|
||||||
val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
|
observeVoiceBroadcastLiveState(voiceBroadcast)
|
||||||
isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
|
fetchPlaylistAndStartPlayback(voiceBroadcast)
|
||||||
fetchPlaylistAndStartPlayback(roomId, eventId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) {
|
private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) {
|
||||||
fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId)
|
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
|
||||||
.onEach(this::updatePlaylist)
|
.onEach { currentVoiceBroadcastEvent = it.getOrNull() }
|
||||||
.launchIn(coroutineScope)
|
.launchIn(sessionScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlaylist(audioEvents: List<MessageAudioEvent>) {
|
private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
|
||||||
val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs }
|
fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
|
||||||
val chunkPositions = sorted
|
.onEach {
|
||||||
.map { it.duration }
|
playlist.setItems(it)
|
||||||
.runningFold(0) { acc, i -> acc + i }
|
onPlaylistUpdated()
|
||||||
.dropLast(1)
|
}
|
||||||
playlist = sorted.mapIndexed { index, messageAudioEvent ->
|
.launchIn(sessionScope)
|
||||||
PlaylistItem(
|
|
||||||
audioEvent = messageAudioEvent,
|
|
||||||
startTime = chunkPositions.getOrNull(index) ?: 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onPlaylistUpdated()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPlaylistUpdated() {
|
private fun onPlaylistUpdated() {
|
||||||
when (playingState) {
|
when (playingState) {
|
||||||
State.PLAYING -> {
|
State.PLAYING -> {
|
||||||
if (nextMediaPlayer == null) {
|
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
||||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
prepareNextMediaPlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State.PAUSED -> {
|
State.PAUSED -> {
|
||||||
if (nextMediaPlayer == null) {
|
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
||||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
prepareNextMediaPlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State.BUFFERING -> {
|
State.BUFFERING -> {
|
||||||
val newMediaContent = getNextAudioContent()
|
val nextItem = playlist.getNextItem()
|
||||||
if (newMediaContent != null) startPlayback()
|
if (nextItem != null) {
|
||||||
|
val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
|
||||||
|
startPlayback(savedPosition?.takeIf { it > 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State.IDLE -> {
|
||||||
|
val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
|
||||||
|
startPlayback(savedPosition?.takeIf { it > 0 })
|
||||||
}
|
}
|
||||||
State.IDLE -> startPlayback()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlayback(sequence: Int? = null, position: Int = 0) {
|
private fun startPlayback(position: Int? = null) {
|
||||||
|
stopPlayer()
|
||||||
|
|
||||||
val playlistItem = when {
|
val playlistItem = when {
|
||||||
sequence != null -> playlist.find { it.audioEvent.sequence == sequence }
|
position != null -> playlist.findByPosition(position)
|
||||||
isLive -> playlist.lastOrNull()
|
currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
|
||||||
else -> playlist.firstOrNull()
|
else -> playlist.firstOrNull()
|
||||||
}
|
}
|
||||||
val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
||||||
val computedSequence = playlistItem.audioEvent.sequence
|
val sequence = playlistItem.audioEvent.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return }
|
||||||
coroutineScope.launch {
|
val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0
|
||||||
|
sessionScope.launch {
|
||||||
try {
|
try {
|
||||||
currentMediaPlayer = prepareMediaPlayer(content)
|
prepareMediaPlayer(content) { mp ->
|
||||||
currentMediaPlayer?.start()
|
currentMediaPlayer = mp
|
||||||
if (position > 0) {
|
playlist.currentSequence = sequence
|
||||||
currentMediaPlayer?.seekTo(position)
|
mp.start()
|
||||||
|
if (sequencePosition > 0) {
|
||||||
|
mp.seekTo(sequencePosition)
|
||||||
|
}
|
||||||
|
playingState = State.PLAYING
|
||||||
|
prepareNextMediaPlayer()
|
||||||
}
|
}
|
||||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
|
||||||
currentSequence = computedSequence
|
|
||||||
withContext(Dispatchers.Main) { playingState = State.PLAYING }
|
|
||||||
nextMediaPlayer = prepareNextMediaPlayer()
|
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Unable to start playback")
|
Timber.e(failure, "Unable to start playback")
|
||||||
throw VoiceFailure.UnableToPlay(failure)
|
throw VoiceFailure.UnableToPlay(failure)
|
||||||
|
@ -229,41 +211,59 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pausePlayback(positionMillis: Int? = null) {
|
||||||
|
if (positionMillis == null) {
|
||||||
|
currentMediaPlayer?.pause()
|
||||||
|
} else {
|
||||||
|
stopPlayer()
|
||||||
|
val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId
|
||||||
|
val duration = playlist.duration.takeIf { it > 0 }
|
||||||
|
if (voiceBroadcastId != null && duration != null) {
|
||||||
|
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playingState = State.PAUSED
|
||||||
|
}
|
||||||
|
|
||||||
private fun resumePlayback() {
|
private fun resumePlayback() {
|
||||||
currentMediaPlayer?.start()
|
if (currentMediaPlayer != null) {
|
||||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
currentMediaPlayer?.start()
|
||||||
playingState = State.PLAYING
|
playingState = State.PLAYING
|
||||||
|
} else {
|
||||||
|
val position = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
|
||||||
|
startPlayback(position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(positionMillis: Int) {
|
override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) {
|
||||||
val duration = getVoiceBroadcastDuration()
|
when {
|
||||||
val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return
|
voiceBroadcast != currentVoiceBroadcast -> {
|
||||||
val audioEvent = playlistItem.audioEvent
|
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||||
val eventPosition = positionMillis - playlistItem.startTime
|
}
|
||||||
|
playingState == State.PLAYING || playingState == State.BUFFERING -> {
|
||||||
Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${audioEvent.sequence}, sequencePosition=$eventPosition")
|
startPlayback(positionMillis)
|
||||||
|
}
|
||||||
tryOrNull { currentMediaPlayer?.stop() }
|
playingState == State.IDLE || playingState == State.PAUSED -> {
|
||||||
release(currentMediaPlayer)
|
pausePlayback(positionMillis)
|
||||||
tryOrNull { nextMediaPlayer?.stop() }
|
}
|
||||||
release(nextMediaPlayer)
|
}
|
||||||
|
|
||||||
startPlayback(audioEvent.sequence, eventPosition)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNextAudioContent(): MessageAudioContent? {
|
private fun prepareNextMediaPlayer() {
|
||||||
val nextSequence = currentSequence?.plus(1)
|
val nextItem = playlist.getNextItem()
|
||||||
?: playlist.lastOrNull()?.audioEvent?.sequence
|
if (nextItem != null) {
|
||||||
?: 1
|
isPreparingNextPlayer = true
|
||||||
return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content
|
sessionScope.launch {
|
||||||
|
prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
|
||||||
|
nextMediaPlayer = mp
|
||||||
|
currentMediaPlayer?.setNextMediaPlayer(mp)
|
||||||
|
isPreparingNextPlayer = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun prepareNextMediaPlayer(): MediaPlayer? {
|
private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent, onPreparedListener: OnPreparedListener): MediaPlayer {
|
||||||
val nextContent = getNextAudioContent() ?: return null
|
|
||||||
return prepareMediaPlayer(nextContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent): MediaPlayer {
|
|
||||||
// Download can fail
|
// Download can fail
|
||||||
val audioFile = try {
|
val audioFile = try {
|
||||||
session.fileService().downloadFile(messageAudioContent)
|
session.fileService().downloadFile(messageAudioContent)
|
||||||
|
@ -284,58 +284,76 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
setDataSource(fis.fd)
|
setDataSource(fis.fd)
|
||||||
setOnInfoListener(mediaPlayerListener)
|
setOnInfoListener(mediaPlayerListener)
|
||||||
setOnErrorListener(mediaPlayerListener)
|
setOnErrorListener(mediaPlayerListener)
|
||||||
|
setOnPreparedListener(onPreparedListener)
|
||||||
setOnCompletionListener(mediaPlayerListener)
|
setOnCompletionListener(mediaPlayerListener)
|
||||||
prepare()
|
prepare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun release(mp: MediaPlayer?) {
|
private fun stopPlayer() {
|
||||||
mp?.apply {
|
tryOrNull { currentMediaPlayer?.stop() }
|
||||||
release()
|
currentMediaPlayer?.release()
|
||||||
setOnInfoListener(null)
|
currentMediaPlayer = null
|
||||||
setOnCompletionListener(null)
|
|
||||||
setOnErrorListener(null)
|
nextMediaPlayer?.release()
|
||||||
|
nextMediaPlayer = null
|
||||||
|
isPreparingNextPlayer = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPlayingStateChanged(playingState: State) {
|
||||||
|
// Notify state change to all the listeners attached to the current voice broadcast id
|
||||||
|
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
||||||
|
when (playingState) {
|
||||||
|
State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
|
||||||
|
State.PAUSED,
|
||||||
|
State.BUFFERING,
|
||||||
|
State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
|
||||||
|
}
|
||||||
|
listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getCurrentPlaybackPosition(): Int? {
|
||||||
|
val playlistPosition = playlist.currentItem?.startTime
|
||||||
|
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
|
||||||
|
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
|
||||||
|
return computedPosition ?: savedPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrentPlaybackPercentage(): Float? {
|
||||||
|
val playlistPosition = playlist.currentItem?.startTime
|
||||||
|
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
|
||||||
|
val duration = playlist.duration.takeIf { it > 0 }
|
||||||
|
val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null
|
||||||
|
val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) }
|
||||||
|
return computedPercentage ?: savedPercentage
|
||||||
|
}
|
||||||
|
|
||||||
private inner class MediaPlayerListener :
|
private inner class MediaPlayerListener :
|
||||||
MediaPlayer.OnInfoListener,
|
MediaPlayer.OnInfoListener,
|
||||||
MediaPlayer.OnPreparedListener,
|
|
||||||
MediaPlayer.OnCompletionListener,
|
MediaPlayer.OnCompletionListener,
|
||||||
MediaPlayer.OnErrorListener {
|
MediaPlayer.OnErrorListener {
|
||||||
|
|
||||||
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||||
when (what) {
|
when (what) {
|
||||||
MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
|
MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
|
||||||
release(currentMediaPlayer)
|
playlist.currentSequence = playlist.currentSequence?.inc()
|
||||||
currentMediaPlayer = mp
|
currentMediaPlayer = mp
|
||||||
currentSequence = currentSequence?.plus(1)
|
nextMediaPlayer = null
|
||||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
playingState = State.PLAYING
|
||||||
|
prepareNextMediaPlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepared(mp: MediaPlayer) {
|
|
||||||
when (mp) {
|
|
||||||
currentMediaPlayer -> {
|
|
||||||
nextMediaPlayer?.let { mp.setNextMediaPlayer(it) }
|
|
||||||
}
|
|
||||||
nextMediaPlayer -> {
|
|
||||||
tryOrNull { currentMediaPlayer?.setNextMediaPlayer(mp) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCompletion(mp: MediaPlayer) {
|
override fun onCompletion(mp: MediaPlayer) {
|
||||||
if (nextMediaPlayer != null) return
|
if (nextMediaPlayer != null) return
|
||||||
val roomId = currentRoomId ?: return
|
|
||||||
val voiceBroadcastId = currentVoiceBroadcastId ?: return
|
|
||||||
val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return
|
|
||||||
isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED
|
|
||||||
|
|
||||||
if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) {
|
val content = currentVoiceBroadcastEvent?.content
|
||||||
|
val isLive = content?.isLive.orFalse()
|
||||||
|
if (!isLive && content?.lastChunkSequence == playlist.currentSequence) {
|
||||||
// We'll not receive new chunks anymore so we can stop the live listening
|
// We'll not receive new chunks anymore so we can stop the live listening
|
||||||
stop()
|
stop()
|
||||||
} else {
|
} else {
|
||||||
|
@ -349,7 +367,48 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0
|
private inner class PlaybackTicker(
|
||||||
|
private var playbackTicker: CountUpTimer? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int)
|
fun startPlaybackTicker(id: String) {
|
||||||
|
playbackTicker?.stop()
|
||||||
|
playbackTicker = CountUpTimer(50L).apply {
|
||||||
|
tickListener = CountUpTimer.TickListener { onPlaybackTick(id) }
|
||||||
|
resume()
|
||||||
|
}
|
||||||
|
onPlaybackTick(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopPlaybackTicker(id: String) {
|
||||||
|
playbackTicker?.stop()
|
||||||
|
playbackTicker = null
|
||||||
|
onPlaybackTick(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPlaybackTick(id: String) {
|
||||||
|
val playbackTime = getCurrentPlaybackPosition()
|
||||||
|
val percentage = getCurrentPlaybackPercentage()
|
||||||
|
when (playingState) {
|
||||||
|
State.PLAYING -> {
|
||||||
|
if (playbackTime != null && percentage != null) {
|
||||||
|
playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State.PAUSED,
|
||||||
|
State.BUFFERING -> {
|
||||||
|
if (playbackTime != null && percentage != null) {
|
||||||
|
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State.IDLE -> {
|
||||||
|
if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) {
|
||||||
|
playbackTracker.stopPlayback(id)
|
||||||
|
} else {
|
||||||
|
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.voicebroadcast.listening
|
||||||
|
|
||||||
|
import im.vector.app.features.voicebroadcast.duration
|
||||||
|
import im.vector.app.features.voicebroadcast.sequence
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
||||||
|
|
||||||
|
class VoiceBroadcastPlaylist(
|
||||||
|
private val items: MutableList<PlaylistItem> = mutableListOf(),
|
||||||
|
) : List<PlaylistItem> by items {
|
||||||
|
|
||||||
|
var currentSequence: Int? = null
|
||||||
|
val currentItem get() = currentSequence?.let { findBySequence(it) }
|
||||||
|
|
||||||
|
val duration
|
||||||
|
get() = items.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0
|
||||||
|
|
||||||
|
fun setItems(audioEvents: List<MessageAudioEvent>) {
|
||||||
|
items.clear()
|
||||||
|
val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs }
|
||||||
|
val chunkPositions = sorted
|
||||||
|
.map { it.duration }
|
||||||
|
.runningFold(0) { acc, i -> acc + i }
|
||||||
|
.dropLast(1)
|
||||||
|
val newItems = sorted.mapIndexed { index, messageAudioEvent ->
|
||||||
|
PlaylistItem(
|
||||||
|
audioEvent = messageAudioEvent,
|
||||||
|
startTime = chunkPositions.getOrNull(index) ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items.addAll(newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
currentSequence = null
|
||||||
|
items.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findByPosition(positionMillis: Int): PlaylistItem? {
|
||||||
|
return items.lastOrNull { it.startTime <= positionMillis }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findBySequence(sequenceNumber: Int): PlaylistItem? {
|
||||||
|
return items.find { it.audioEvent.sequence == sequenceNumber }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNextItem() = findBySequence(currentSequence?.plus(1) ?: 1)
|
||||||
|
|
||||||
|
fun firstOrNull() = findBySequence(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int)
|
|
@ -19,18 +19,21 @@ package im.vector.app.features.voicebroadcast.listening.usecase
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
|
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
|
||||||
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import im.vector.app.features.voicebroadcast.sequence
|
import im.vector.app.features.voicebroadcast.sequence
|
||||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.runningReduce
|
import kotlinx.coroutines.flow.runningReduce
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||||
|
@ -44,19 +47,19 @@ import javax.inject.Inject
|
||||||
*/
|
*/
|
||||||
class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun execute(roomId: String, voiceBroadcastId: String): Flow<List<MessageAudioEvent>> {
|
fun execute(voiceBroadcast: VoiceBroadcast): Flow<List<MessageAudioEvent>> {
|
||||||
val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow()
|
val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow()
|
||||||
val room = session.roomService().getRoom(roomId) ?: return emptyFlow()
|
val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow()
|
||||||
val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
|
val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
|
||||||
|
|
||||||
// Get initial chunks
|
// Get initial chunks
|
||||||
val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcastId)
|
val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
|
||||||
.mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } }
|
.mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } }
|
||||||
|
|
||||||
val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)
|
val voiceBroadcastEvent = runBlocking { getVoiceBroadcastEventUseCase.execute(voiceBroadcast).firstOrNull()?.getOrNull() }
|
||||||
val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
|
val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
|
||||||
|
|
||||||
return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) {
|
return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) {
|
||||||
|
@ -82,7 +85,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
||||||
lastSequence = stopEvent.content?.lastChunkSequence
|
lastSequence = stopEvent.content?.lastChunkSequence
|
||||||
}
|
}
|
||||||
|
|
||||||
val newChunks = newEvents.mapToChunkEvents(voiceBroadcastId, voiceBroadcastEvent.root.senderId)
|
val newChunks = newEvents.mapToChunkEvents(voiceBroadcast.voiceBroadcastId, voiceBroadcastEvent.root.senderId)
|
||||||
|
|
||||||
// Notify about new chunks
|
// Notify about new chunks
|
||||||
if (newChunks.isNotEmpty()) {
|
if (newChunks.isNotEmpty()) {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.voicebroadcast.model
|
||||||
|
|
||||||
|
data class VoiceBroadcast(
|
||||||
|
val voiceBroadcastId: String,
|
||||||
|
val roomId: String,
|
||||||
|
)
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.voicebroadcast.usecase
|
||||||
|
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
|
import org.matrix.android.sdk.flow.flow
|
||||||
|
import org.matrix.android.sdk.flow.unwrap
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetVoiceBroadcastEventUseCase @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
|
||||||
|
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
|
||||||
|
|
||||||
|
Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast")
|
||||||
|
|
||||||
|
val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent()
|
||||||
|
val latestEvent = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
|
||||||
|
.mapNotNull { it.root.asVoiceBroadcastEvent() }
|
||||||
|
.maxByOrNull { it.root.originServerTs ?: 0 }
|
||||||
|
?: initialEvent
|
||||||
|
|
||||||
|
return when (latestEvent?.content?.voiceBroadcastState) {
|
||||||
|
null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional())
|
||||||
|
else -> {
|
||||||
|
room.flow()
|
||||||
|
.liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty()))
|
||||||
|
.unwrap()
|
||||||
|
.mapNotNull { it.asVoiceBroadcastEvent() }
|
||||||
|
.filter { it.reference?.eventId == voiceBroadcast.voiceBroadcastId }
|
||||||
|
.map { it.toOptional() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast.usecase
|
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class GetVoiceBroadcastUseCase @Inject constructor(
|
|
||||||
private val session: Session,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun execute(roomId: String, eventId: String): VoiceBroadcastEvent? {
|
|
||||||
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
|
|
||||||
|
|
||||||
Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $eventId")
|
|
||||||
|
|
||||||
val initialEvent = room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() // Fallback to initial event
|
|
||||||
val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId).sortedBy { it.root.originServerTs }
|
|
||||||
return relatedEvents.mapNotNull { it.root.asVoiceBroadcastEvent() }.lastOrNull() ?: initialEvent
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -100,10 +100,12 @@
|
||||||
android:id="@+id/fastBackwardButton"
|
android:id="@+id/fastBackwardButton"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:background="@android:color/transparent"
|
android:background="@drawable/bg_rounded_button"
|
||||||
android:contentDescription="@string/a11y_voice_broadcast_fast_backward"
|
android:contentDescription="@string/a11y_voice_broadcast_fast_backward"
|
||||||
android:src="@drawable/ic_player_backward_30"
|
android:src="@drawable/ic_player_backward_30"
|
||||||
app:tint="?vctr_content_secondary" />
|
android:visibility="invisible"
|
||||||
|
app:tint="?vctr_content_secondary"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/playPauseButton"
|
android:id="@+id/playPauseButton"
|
||||||
|
@ -121,16 +123,20 @@
|
||||||
android:layout_height="@dimen/voice_broadcast_player_button_size"
|
android:layout_height="@dimen/voice_broadcast_player_button_size"
|
||||||
android:contentDescription="@string/a11y_voice_broadcast_buffering"
|
android:contentDescription="@string/a11y_voice_broadcast_buffering"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
android:indeterminateTint="?vctr_content_secondary" />
|
android:indeterminateTint="?vctr_content_secondary"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/fastForwardButton"
|
android:id="@+id/fastForwardButton"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:background="@android:color/transparent"
|
android:background="@drawable/bg_rounded_button"
|
||||||
android:contentDescription="@string/a11y_voice_broadcast_fast_forward"
|
android:contentDescription="@string/a11y_voice_broadcast_fast_forward"
|
||||||
android:src="@drawable/ic_player_forward_30"
|
android:src="@drawable/ic_player_forward_30"
|
||||||
app:tint="?vctr_content_secondary" />
|
android:visibility="invisible"
|
||||||
|
app:tint="?vctr_content_secondary"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<SeekBar
|
<SeekBar
|
||||||
android:id="@+id/seekBar"
|
android:id="@+id/seekBar"
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.core.notification
|
||||||
|
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
||||||
|
import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase
|
||||||
|
import im.vector.app.test.fakes.FakeSession
|
||||||
|
import im.vector.app.test.fakes.FakeVectorPreferences
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_SESSION_ID = "session-id"
|
||||||
|
|
||||||
|
class UpdateEnableNotificationsSettingOnChangeUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeSession = FakeSession().also { it.givenSessionId(A_SESSION_ID) }
|
||||||
|
private val fakeVectorPreferences = FakeVectorPreferences()
|
||||||
|
private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase()
|
||||||
|
|
||||||
|
private val updateEnableNotificationsSettingOnChangeUseCase = UpdateEnableNotificationsSettingOnChangeUseCase(
|
||||||
|
vectorPreferences = fakeVectorPreferences.instance,
|
||||||
|
getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given notifications are enabled when execute then setting is updated to true`() = runTest {
|
||||||
|
// Given
|
||||||
|
fakeGetNotificationsStatusUseCase.givenExecuteReturns(
|
||||||
|
fakeSession,
|
||||||
|
A_SESSION_ID,
|
||||||
|
NotificationsStatus.ENABLED,
|
||||||
|
)
|
||||||
|
fakeVectorPreferences.givenSetNotificationEnabledForDevice()
|
||||||
|
|
||||||
|
// When
|
||||||
|
updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
fakeVectorPreferences.verifySetNotificationEnabledForDevice(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given notifications are disabled when execute then setting is updated to false`() = runTest {
|
||||||
|
// Given
|
||||||
|
fakeGetNotificationsStatusUseCase.givenExecuteReturns(
|
||||||
|
fakeSession,
|
||||||
|
A_SESSION_ID,
|
||||||
|
NotificationsStatus.DISABLED,
|
||||||
|
)
|
||||||
|
fakeVectorPreferences.givenSetNotificationEnabledForDevice()
|
||||||
|
|
||||||
|
// When
|
||||||
|
updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
fakeVectorPreferences.verifySetNotificationEnabledForDevice(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given notifications toggle is not supported when execute then nothing is done`() = runTest {
|
||||||
|
// Given
|
||||||
|
fakeGetNotificationsStatusUseCase.givenExecuteReturns(
|
||||||
|
fakeSession,
|
||||||
|
A_SESSION_ID,
|
||||||
|
NotificationsStatus.NOT_SUPPORTED,
|
||||||
|
)
|
||||||
|
fakeVectorPreferences.givenSetNotificationEnabledForDevice()
|
||||||
|
|
||||||
|
// When
|
||||||
|
updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
fakeVectorPreferences.verifySetNotificationEnabledForDevice(true, inverse = true)
|
||||||
|
fakeVectorPreferences.verifySetNotificationEnabledForDevice(false, inverse = true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,6 @@ import im.vector.app.test.fixtures.CryptoDeviceInfoFixture.aCryptoDeviceInfo
|
||||||
import im.vector.app.test.fixtures.PusherFixture
|
import im.vector.app.test.fixtures.PusherFixture
|
||||||
import im.vector.app.test.fixtures.SessionParamsFixture
|
import im.vector.app.test.fixtures.SessionParamsFixture
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
|
||||||
|
@ -101,19 +100,4 @@ class PushersManagerTest {
|
||||||
|
|
||||||
pusher shouldBeEqualTo expectedPusher
|
pusher shouldBeEqualTo expectedPusher
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when togglePusherForCurrentSession, then do service toggle pusher`() = runTest {
|
|
||||||
val deviceId = "device_id"
|
|
||||||
val sessionParams = SessionParamsFixture.aSessionParams(
|
|
||||||
credentials = CredentialsFixture.aCredentials(deviceId = deviceId)
|
|
||||||
)
|
|
||||||
session.givenSessionParams(sessionParams)
|
|
||||||
val pusher = PusherFixture.aPusher(deviceId = deviceId)
|
|
||||||
pushersService.givenGetPushers(listOf(pusher))
|
|
||||||
|
|
||||||
pushersManager.togglePusherForCurrentSession(true)
|
|
||||||
|
|
||||||
pushersService.verifyTogglePusherCalled(pusher, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.core.session
|
||||||
import im.vector.app.core.extensions.startSyncing
|
import im.vector.app.core.extensions.startSyncing
|
||||||
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
|
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
|
||||||
import im.vector.app.test.fakes.FakeContext
|
import im.vector.app.test.fakes.FakeContext
|
||||||
|
import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater
|
||||||
import im.vector.app.test.fakes.FakeSession
|
import im.vector.app.test.fakes.FakeSession
|
||||||
import im.vector.app.test.fakes.FakeVectorPreferences
|
import im.vector.app.test.fakes.FakeVectorPreferences
|
||||||
import im.vector.app.test.fakes.FakeWebRtcCallManager
|
import im.vector.app.test.fakes.FakeWebRtcCallManager
|
||||||
|
@ -43,12 +44,14 @@ class ConfigureAndStartSessionUseCaseTest {
|
||||||
private val fakeWebRtcCallManager = FakeWebRtcCallManager()
|
private val fakeWebRtcCallManager = FakeWebRtcCallManager()
|
||||||
private val fakeUpdateMatrixClientInfoUseCase = mockk<UpdateMatrixClientInfoUseCase>()
|
private val fakeUpdateMatrixClientInfoUseCase = mockk<UpdateMatrixClientInfoUseCase>()
|
||||||
private val fakeVectorPreferences = FakeVectorPreferences()
|
private val fakeVectorPreferences = FakeVectorPreferences()
|
||||||
|
private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater()
|
||||||
|
|
||||||
private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase(
|
private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase(
|
||||||
context = fakeContext.instance,
|
context = fakeContext.instance,
|
||||||
webRtcCallManager = fakeWebRtcCallManager.instance,
|
webRtcCallManager = fakeWebRtcCallManager.instance,
|
||||||
updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase,
|
updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase,
|
||||||
vectorPreferences = fakeVectorPreferences.instance,
|
vectorPreferences = fakeVectorPreferences.instance,
|
||||||
|
enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
@ -68,6 +71,7 @@ class ConfigureAndStartSessionUseCaseTest {
|
||||||
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
||||||
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
||||||
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
|
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
|
||||||
|
fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
|
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
|
||||||
|
@ -87,6 +91,7 @@ class ConfigureAndStartSessionUseCaseTest {
|
||||||
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
||||||
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
||||||
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false)
|
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false)
|
||||||
|
fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
|
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
|
||||||
|
@ -106,6 +111,7 @@ class ConfigureAndStartSessionUseCaseTest {
|
||||||
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
||||||
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
||||||
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
|
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
|
||||||
|
fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false)
|
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false)
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.settings.devices.v2.notification
|
||||||
|
|
||||||
|
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
|
||||||
|
import im.vector.app.test.fakes.FakeSession
|
||||||
|
import im.vector.app.test.fakes.givenAsFlow
|
||||||
|
import im.vector.app.test.fixtures.aHomeServerCapabilities
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true)
|
||||||
|
|
||||||
|
class CanTogglePushNotificationsViaPusherUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeSession = FakeSession()
|
||||||
|
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
|
||||||
|
|
||||||
|
private val canTogglePushNotificationsViaPusherUseCase =
|
||||||
|
CanTogglePushNotificationsViaPusherUseCase()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
fakeFlowLiveDataConversions.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given current session when execute then flow of the toggle capability is returned`() = runTest {
|
||||||
|
// Given
|
||||||
|
fakeSession
|
||||||
|
.fakeHomeServerCapabilitiesService
|
||||||
|
.givenCapabilitiesLiveReturns(A_HOMESERVER_CAPABILITIES)
|
||||||
|
.givenAsFlow()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = canTogglePushNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.settings.devices.v2.notification
|
package im.vector.app.features.settings.devices.v2.notification
|
||||||
|
|
||||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
import im.vector.app.test.fakes.FakeSession
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -26,18 +26,15 @@ private const val A_DEVICE_ID = "device-id"
|
||||||
|
|
||||||
class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
||||||
|
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
private val fakeSession = FakeSession()
|
||||||
|
|
||||||
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase =
|
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase =
|
||||||
CheckIfCanTogglePushNotificationsViaAccountDataUseCase(
|
CheckIfCanTogglePushNotificationsViaAccountDataUseCase()
|
||||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given current session and an account data for the device id when execute then result is true`() {
|
fun `given current session and an account data for the device id when execute then result is true`() {
|
||||||
// Given
|
// Given
|
||||||
fakeActiveSessionHolder
|
fakeSession
|
||||||
.fakeSession
|
|
||||||
.accountDataService()
|
.accountDataService()
|
||||||
.givenGetUserAccountDataEventReturns(
|
.givenGetUserAccountDataEventReturns(
|
||||||
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
||||||
|
@ -45,7 +42,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
|
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
result shouldBeEqualTo true
|
result shouldBeEqualTo true
|
||||||
|
@ -54,8 +51,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given current session and NO account data for the device id when execute then result is false`() {
|
fun `given current session and NO account data for the device id when execute then result is false`() {
|
||||||
// Given
|
// Given
|
||||||
fakeActiveSessionHolder
|
fakeSession
|
||||||
.fakeSession
|
|
||||||
.accountDataService()
|
.accountDataService()
|
||||||
.givenGetUserAccountDataEventReturns(
|
.givenGetUserAccountDataEventReturns(
|
||||||
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
||||||
|
@ -63,7 +59,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
|
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
result shouldBeEqualTo false
|
result shouldBeEqualTo false
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.settings.devices.v2.notification
|
package im.vector.app.features.settings.devices.v2.notification
|
||||||
|
|
||||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
import im.vector.app.test.fakes.FakeSession
|
||||||
import im.vector.app.test.fixtures.aHomeServerCapabilities
|
import im.vector.app.test.fixtures.aHomeServerCapabilities
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -25,37 +25,22 @@ private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyToggl
|
||||||
|
|
||||||
class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest {
|
class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest {
|
||||||
|
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
private val fakeSession = FakeSession()
|
||||||
|
|
||||||
private val checkIfCanTogglePushNotificationsViaPusherUseCase =
|
private val checkIfCanTogglePushNotificationsViaPusherUseCase =
|
||||||
CheckIfCanTogglePushNotificationsViaPusherUseCase(
|
CheckIfCanTogglePushNotificationsViaPusherUseCase()
|
||||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given current session when execute then toggle capability is returned`() {
|
fun `given current session when execute then toggle capability is returned`() {
|
||||||
// Given
|
// Given
|
||||||
fakeActiveSessionHolder
|
fakeSession
|
||||||
.fakeSession
|
|
||||||
.fakeHomeServerCapabilitiesService
|
.fakeHomeServerCapabilitiesService
|
||||||
.givenCapabilities(A_HOMESERVER_CAPABILITIES)
|
.givenCapabilities(A_HOMESERVER_CAPABILITIES)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute()
|
val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices
|
result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `given no current session when execute then false is returned`() {
|
|
||||||
// Given
|
|
||||||
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
|
|
||||||
|
|
||||||
// When
|
|
||||||
val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute()
|
|
||||||
|
|
||||||
// Then
|
|
||||||
result shouldBeEqualTo false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
package im.vector.app.features.settings.devices.v2.notification
|
package im.vector.app.features.settings.devices.v2.notification
|
||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
import im.vector.app.test.fakes.FakeSession
|
||||||
import im.vector.app.test.fixtures.PusherFixture
|
import im.vector.app.test.fixtures.PusherFixture
|
||||||
import im.vector.app.test.testDispatcher
|
import im.vector.app.test.testDispatcher
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -25,6 +25,7 @@ import io.mockk.mockk
|
||||||
import io.mockk.verifyOrder
|
import io.mockk.verifyOrder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
|
@ -44,17 +45,16 @@ class GetNotificationsStatusUseCaseTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
private val fakeSession = FakeSession()
|
||||||
private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase =
|
|
||||||
mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
|
|
||||||
private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
|
private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
|
||||||
mockk<CheckIfCanTogglePushNotificationsViaAccountDataUseCase>()
|
mockk<CheckIfCanTogglePushNotificationsViaAccountDataUseCase>()
|
||||||
|
private val fakeCanTogglePushNotificationsViaPusherUseCase =
|
||||||
|
mockk<CanTogglePushNotificationsViaPusherUseCase>()
|
||||||
|
|
||||||
private val getNotificationsStatusUseCase =
|
private val getNotificationsStatusUseCase =
|
||||||
GetNotificationsStatusUseCase(
|
GetNotificationsStatusUseCase(
|
||||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
|
||||||
checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
|
|
||||||
checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase,
|
checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase,
|
||||||
|
canTogglePushNotificationsViaPusherUseCase = fakeCanTogglePushNotificationsViaPusherUseCase,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
@ -67,33 +67,21 @@ class GetNotificationsStatusUseCaseTest {
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `given NO current session when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
|
|
||||||
// Given
|
|
||||||
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
|
|
||||||
|
|
||||||
// When
|
|
||||||
val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
|
fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
|
||||||
// Given
|
// Given
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false
|
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false
|
every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID)
|
val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED
|
result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED
|
||||||
verifyOrder {
|
verifyOrder {
|
||||||
// we should first check account data
|
// we should first check account data
|
||||||
fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
|
fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||||
fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute()
|
fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,12 +94,12 @@ class GetNotificationsStatusUseCaseTest {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
|
fakeSession.pushersService().givenPushersLive(pushers)
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true
|
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false
|
every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID)
|
val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
|
result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
|
||||||
|
@ -120,8 +108,7 @@ class GetNotificationsStatusUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest {
|
fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest {
|
||||||
// Given
|
// Given
|
||||||
fakeActiveSessionHolder
|
fakeSession
|
||||||
.fakeSession
|
|
||||||
.accountDataService()
|
.accountDataService()
|
||||||
.givenGetUserAccountDataEventReturns(
|
.givenGetUserAccountDataEventReturns(
|
||||||
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
||||||
|
@ -129,11 +116,11 @@ class GetNotificationsStatusUseCaseTest {
|
||||||
isSilenced = false
|
isSilenced = false
|
||||||
).toContent(),
|
).toContent(),
|
||||||
)
|
)
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false
|
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns true
|
every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID)
|
val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
|
result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
|
||||||
|
|
|
@ -49,10 +49,11 @@ class TogglePushNotificationUseCaseTest {
|
||||||
PusherFixture.aPusher(deviceId = sessionId, enabled = false),
|
PusherFixture.aPusher(deviceId = sessionId, enabled = false),
|
||||||
PusherFixture.aPusher(deviceId = "another id", enabled = false)
|
PusherFixture.aPusher(deviceId = "another id", enabled = false)
|
||||||
)
|
)
|
||||||
activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
|
val fakeSession = activeSessionHolder.fakeSession
|
||||||
activeSessionHolder.fakeSession.pushersService().givenGetPushers(pushers)
|
fakeSession.pushersService().givenPushersLive(pushers)
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true
|
fakeSession.pushersService().givenGetPushers(pushers)
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns false
|
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true
|
||||||
|
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false
|
||||||
|
|
||||||
// When
|
// When
|
||||||
togglePushNotificationUseCase.execute(sessionId, true)
|
togglePushNotificationUseCase.execute(sessionId, true)
|
||||||
|
@ -69,13 +70,14 @@ class TogglePushNotificationUseCaseTest {
|
||||||
PusherFixture.aPusher(deviceId = sessionId, enabled = false),
|
PusherFixture.aPusher(deviceId = sessionId, enabled = false),
|
||||||
PusherFixture.aPusher(deviceId = "another id", enabled = false)
|
PusherFixture.aPusher(deviceId = "another id", enabled = false)
|
||||||
)
|
)
|
||||||
activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
|
val fakeSession = activeSessionHolder.fakeSession
|
||||||
activeSessionHolder.fakeSession.accountDataService().givenGetUserAccountDataEventReturns(
|
fakeSession.pushersService().givenPushersLive(pushers)
|
||||||
|
fakeSession.accountDataService().givenGetUserAccountDataEventReturns(
|
||||||
UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
|
UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
|
||||||
LocalNotificationSettingsContent(isSilenced = true).toContent()
|
LocalNotificationSettingsContent(isSilenced = true).toContent()
|
||||||
)
|
)
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false
|
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
|
||||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns true
|
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true
|
||||||
|
|
||||||
// When
|
// When
|
||||||
togglePushNotificationUseCase.execute(sessionId, true)
|
togglePushNotificationUseCase.execute(sessionId, true)
|
||||||
|
|
|
@ -22,11 +22,11 @@ import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.test.MavericksTestRule
|
import com.airbnb.mvrx.test.MavericksTestRule
|
||||||
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
|
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
|
||||||
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
|
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
|
|
||||||
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
||||||
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
|
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||||
|
import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase
|
||||||
import im.vector.app.test.fakes.FakePendingAuthHandler
|
import im.vector.app.test.fakes.FakePendingAuthHandler
|
||||||
import im.vector.app.test.fakes.FakeSharedPreferences
|
import im.vector.app.test.fakes.FakeSharedPreferences
|
||||||
import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
|
import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
|
||||||
|
@ -76,7 +76,7 @@ class SessionOverviewViewModelTest {
|
||||||
private val fakePendingAuthHandler = FakePendingAuthHandler()
|
private val fakePendingAuthHandler = FakePendingAuthHandler()
|
||||||
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
|
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
|
||||||
private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase()
|
private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase()
|
||||||
private val fakeGetNotificationsStatusUseCase = mockk<GetNotificationsStatusUseCase>()
|
private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase()
|
||||||
private val notificationsStatus = NotificationsStatus.ENABLED
|
private val notificationsStatus = NotificationsStatus.ENABLED
|
||||||
private val fakeSharedPreferences = FakeSharedPreferences()
|
private val fakeSharedPreferences = FakeSharedPreferences()
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ class SessionOverviewViewModelTest {
|
||||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||||
refreshDevicesUseCase = refreshDevicesUseCase,
|
refreshDevicesUseCase = refreshDevicesUseCase,
|
||||||
togglePushNotificationUseCase = togglePushNotificationUseCase.instance,
|
togglePushNotificationUseCase = togglePushNotificationUseCase.instance,
|
||||||
getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase,
|
getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance,
|
||||||
sharedPreferences = fakeSharedPreferences,
|
sharedPreferences = fakeSharedPreferences,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -101,7 +101,11 @@ class SessionOverviewViewModelTest {
|
||||||
every { SystemClock.elapsedRealtime() } returns 1234
|
every { SystemClock.elapsedRealtime() } returns 1234
|
||||||
|
|
||||||
givenVerificationService()
|
givenVerificationService()
|
||||||
every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus)
|
fakeGetNotificationsStatusUseCase.givenExecuteReturns(
|
||||||
|
fakeActiveSessionHolder.fakeSession,
|
||||||
|
A_SESSION_ID_1,
|
||||||
|
notificationsStatus
|
||||||
|
)
|
||||||
fakeSharedPreferences.givenSessionManagerShowIpAddress(false)
|
fakeSharedPreferences.givenSessionManagerShowIpAddress(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,13 +420,10 @@ class SessionOverviewViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when viewModel init, then observe pushers and emit to state`() {
|
fun `when viewModel init, then observe pushers and emit to state`() {
|
||||||
val notificationStatus = NotificationsStatus.ENABLED
|
|
||||||
every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationStatus)
|
|
||||||
|
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.test()
|
viewModel.test()
|
||||||
.assertLatestState { state -> state.notificationsStatus == notificationStatus }
|
.assertLatestState { state -> state.notificationsStatus == notificationsStatus }
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.settings.notifications
|
||||||
|
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
|
||||||
|
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||||
|
import im.vector.app.test.fakes.FakePushersManager
|
||||||
|
import im.vector.app.test.fakes.FakeUnifiedPushHelper
|
||||||
|
import io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_SESSION_ID = "session-id"
|
||||||
|
|
||||||
|
class DisableNotificationsForCurrentSessionUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
|
private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
|
||||||
|
private val fakePushersManager = FakePushersManager()
|
||||||
|
private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
|
||||||
|
private val fakeTogglePushNotificationUseCase = mockk<TogglePushNotificationUseCase>()
|
||||||
|
|
||||||
|
private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase(
|
||||||
|
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||||
|
unifiedPushHelper = fakeUnifiedPushHelper.instance,
|
||||||
|
pushersManager = fakePushersManager.instance,
|
||||||
|
checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
|
||||||
|
togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest {
|
||||||
|
// Given
|
||||||
|
val fakeSession = fakeActiveSessionHolder.fakeSession
|
||||||
|
fakeSession.givenSessionId(A_SESSION_ID)
|
||||||
|
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true
|
||||||
|
coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
|
||||||
|
|
||||||
|
// When
|
||||||
|
disableNotificationsForCurrentSessionUseCase.execute()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest {
|
||||||
|
// Given
|
||||||
|
val fakeSession = fakeActiveSessionHolder.fakeSession
|
||||||
|
fakeSession.givenSessionId(A_SESSION_ID)
|
||||||
|
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
|
||||||
|
fakeUnifiedPushHelper.givenUnregister(fakePushersManager.instance)
|
||||||
|
|
||||||
|
// When
|
||||||
|
disableNotificationsForCurrentSessionUseCase.execute()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
fakeUnifiedPushHelper.verifyUnregister(fakePushersManager.instance)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.settings.notifications
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
|
||||||
|
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||||
|
import im.vector.app.test.fakes.FakeFcmHelper
|
||||||
|
import im.vector.app.test.fakes.FakePushersManager
|
||||||
|
import im.vector.app.test.fakes.FakeUnifiedPushHelper
|
||||||
|
import io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_SESSION_ID = "session-id"
|
||||||
|
|
||||||
|
class EnableNotificationsForCurrentSessionUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
|
private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
|
||||||
|
private val fakePushersManager = FakePushersManager()
|
||||||
|
private val fakeFcmHelper = FakeFcmHelper()
|
||||||
|
private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
|
||||||
|
private val fakeTogglePushNotificationUseCase = mockk<TogglePushNotificationUseCase>()
|
||||||
|
|
||||||
|
private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase(
|
||||||
|
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||||
|
unifiedPushHelper = fakeUnifiedPushHelper.instance,
|
||||||
|
pushersManager = fakePushersManager.instance,
|
||||||
|
fcmHelper = fakeFcmHelper.instance,
|
||||||
|
checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
|
||||||
|
togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no existing pusher for current session when execute then a new pusher is registered`() = runTest {
|
||||||
|
// Given
|
||||||
|
val fragmentActivity = mockk<FragmentActivity>()
|
||||||
|
fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
|
||||||
|
fakeUnifiedPushHelper.givenRegister(fragmentActivity)
|
||||||
|
fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
|
||||||
|
fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance)
|
||||||
|
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns false
|
||||||
|
|
||||||
|
// When
|
||||||
|
enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
fakeUnifiedPushHelper.verifyRegister(fragmentActivity)
|
||||||
|
fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance, registerPusher = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given toggle via Pusher is possible when execute then current pusher is toggled to true`() = runTest {
|
||||||
|
// Given
|
||||||
|
val fragmentActivity = mockk<FragmentActivity>()
|
||||||
|
fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk())
|
||||||
|
val fakeSession = fakeActiveSessionHolder.fakeSession
|
||||||
|
fakeSession.givenSessionId(A_SESSION_ID)
|
||||||
|
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns true
|
||||||
|
coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
|
||||||
|
|
||||||
|
// When
|
||||||
|
enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, true) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.core.notification.EnableNotificationsSettingUpdater
|
||||||
|
import io.mockk.justRun
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
|
||||||
|
class FakeEnableNotificationsSettingUpdater {
|
||||||
|
|
||||||
|
val instance = mockk<EnableNotificationsSettingUpdater>()
|
||||||
|
|
||||||
|
fun givenOnSessionsStarted(session: Session) {
|
||||||
|
justRun { instance.onSessionsStarted(session) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import im.vector.app.core.pushers.FcmHelper
|
||||||
|
import im.vector.app.core.pushers.PushersManager
|
||||||
|
import io.mockk.justRun
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
|
||||||
|
class FakeFcmHelper {
|
||||||
|
|
||||||
|
val instance = mockk<FcmHelper>()
|
||||||
|
|
||||||
|
fun givenEnsureFcmTokenIsRetrieved(
|
||||||
|
fragmentActivity: FragmentActivity,
|
||||||
|
pushersManager: PushersManager,
|
||||||
|
) {
|
||||||
|
justRun { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyEnsureFcmTokenIsRetrieved(
|
||||||
|
fragmentActivity: FragmentActivity,
|
||||||
|
pushersManager: PushersManager,
|
||||||
|
registerPusher: Boolean,
|
||||||
|
) {
|
||||||
|
verify { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, registerPusher) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
|
||||||
|
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
|
||||||
|
class FakeGetNotificationsStatusUseCase {
|
||||||
|
|
||||||
|
val instance = mockk<GetNotificationsStatusUseCase>()
|
||||||
|
|
||||||
|
fun givenExecuteReturns(
|
||||||
|
session: Session,
|
||||||
|
sessionId: String,
|
||||||
|
notificationsStatus: NotificationsStatus
|
||||||
|
) {
|
||||||
|
every { instance.execute(session, sessionId) } returns flowOf(notificationsStatus)
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,14 +16,24 @@
|
||||||
|
|
||||||
package im.vector.app.test.fakes
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
|
|
||||||
class FakeHomeServerCapabilitiesService : HomeServerCapabilitiesService by mockk() {
|
class FakeHomeServerCapabilitiesService : HomeServerCapabilitiesService by mockk() {
|
||||||
|
|
||||||
fun givenCapabilities(homeServerCapabilities: HomeServerCapabilities) {
|
fun givenCapabilities(homeServerCapabilities: HomeServerCapabilities) {
|
||||||
every { getHomeServerCapabilities() } returns homeServerCapabilities
|
every { getHomeServerCapabilities() } returns homeServerCapabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenCapabilitiesLiveReturns(homeServerCapabilities: HomeServerCapabilities): LiveData<Optional<HomeServerCapabilities>> {
|
||||||
|
return MutableLiveData(homeServerCapabilities.toOptional()).also {
|
||||||
|
every { getHomeServerCapabilitiesLive() } returns it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.core.pushers.PushersManager
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.matrix.android.sdk.api.session.pushers.Pusher
|
||||||
|
|
||||||
|
class FakePushersManager {
|
||||||
|
|
||||||
|
val instance = mockk<PushersManager>()
|
||||||
|
|
||||||
|
fun givenGetPusherForCurrentSessionReturns(pusher: Pusher?) {
|
||||||
|
every { instance.getPusherForCurrentSession() } returns pusher
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import im.vector.app.core.pushers.PushersManager
|
||||||
|
import im.vector.app.core.pushers.UnifiedPushHelper
|
||||||
|
import io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
|
||||||
|
class FakeUnifiedPushHelper {
|
||||||
|
|
||||||
|
val instance = mockk<UnifiedPushHelper>()
|
||||||
|
|
||||||
|
fun givenRegister(fragmentActivity: FragmentActivity) {
|
||||||
|
every { instance.register(fragmentActivity, any()) } answers {
|
||||||
|
secondArg<Runnable>().run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyRegister(fragmentActivity: FragmentActivity) {
|
||||||
|
verify { instance.register(fragmentActivity, any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenUnregister(pushersManager: PushersManager) {
|
||||||
|
coJustRun { instance.unregister(pushersManager) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyUnregister(pushersManager: PushersManager) {
|
||||||
|
coVerify { instance.unregister(pushersManager) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) {
|
||||||
|
every { instance.isEmbeddedDistributor() } returns isEmbedded
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ package im.vector.app.test.fakes
|
||||||
|
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.justRun
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
|
||||||
|
@ -42,5 +43,13 @@ class FakeVectorPreferences {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenTextFormatting(isEnabled: Boolean) =
|
fun givenTextFormatting(isEnabled: Boolean) =
|
||||||
every { instance.isTextFormattingEnabled() } returns isEnabled
|
every { instance.isTextFormattingEnabled() } returns isEnabled
|
||||||
|
|
||||||
|
fun givenSetNotificationEnabledForDevice() {
|
||||||
|
justRun { instance.setNotificationEnabledForDevice(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifySetNotificationEnabledForDevice(enabled: Boolean, inverse: Boolean = false) {
|
||||||
|
verify(inverse = inverse) { instance.setNotificationEnabledForDevice(enabled) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue