mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 20:29:10 +03:00
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
74 changed files with 1803 additions and 562 deletions
1
changelog.d/7496.wip
Normal file
1
changelog.d/7496.wip
Normal file
|
@ -0,0 +1 @@
|
|||
[Voice Broadcast] Add seekbar in listening tile
|
1
changelog.d/7512.feature
Normal file
1
changelog.d/7512.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Push notifications toggle: align implementation for current session
|
1
changelog.d/7514.sdk
Normal file
1
changelog.d/7514.sdk
Normal file
|
@ -0,0 +1 @@
|
|||
[Metrics] Add `SpannableMetricPlugin` to support spans within transactions.
|
1
changelog.d/7533.bugfix
Normal file
1
changelog.d/7533.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix description of verified sessions
|
|
@ -8,7 +8,7 @@ ext.versions = [
|
|||
|
||||
def gradle = "7.3.1"
|
||||
// Ref: https://kotlinlang.org/releases.html
|
||||
def kotlin = "1.7.20"
|
||||
def kotlin = "1.7.21"
|
||||
def kotlinCoroutines = "1.6.4"
|
||||
def dagger = "2.44"
|
||||
def appDistribution = "16.0.0-beta05"
|
||||
|
|
|
@ -103,14 +103,12 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
views.videoView.setOnPreparedListener {
|
||||
stopTimer()
|
||||
countUpTimer = CountUpTimer(100).also {
|
||||
it.tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
val duration = views.videoView.duration
|
||||
val progress = views.videoView.currentPosition
|
||||
val isPlaying = views.videoView.isPlaying
|
||||
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
||||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||
}
|
||||
it.tickListener = CountUpTimer.TickListener {
|
||||
val duration = views.videoView.duration
|
||||
val progress = views.videoView.currentPosition
|
||||
val isPlaying = views.videoView.isPlaying
|
||||
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
||||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||
}
|
||||
it.resume()
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class CountUpTimer(private val intervalInMs: Long = 1_000) {
|
|||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
interface TickListener {
|
||||
fun interface TickListener {
|
||||
fun onTick(milliseconds: Long)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1679,7 +1679,8 @@
|
|||
<string name="create_new_room">Create New Room</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_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="please_wait">"Please wait…"</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">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">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">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>
|
||||
|
|
|
@ -17,25 +17,51 @@
|
|||
package org.matrix.android.sdk.api.extensions
|
||||
|
||||
import org.matrix.android.sdk.api.metrics.MetricPlugin
|
||||
import org.matrix.android.sdk.api.metrics.SpannableMetricPlugin
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* Executes the given [block] while measuring the transaction.
|
||||
*
|
||||
* @param block Action/Task to be executed within this span.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun measureMetric(metricMeasurementPlugins: List<MetricPlugin>, block: () -> Unit) {
|
||||
inline fun List<MetricPlugin>.measureMetric(block: () -> Unit) {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
try {
|
||||
metricMeasurementPlugins.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
|
||||
this.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
|
||||
block()
|
||||
} 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
|
||||
} 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
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
/**
|
||||
* This interface defines a method to retrieve the homeserver capabilities.
|
||||
*/
|
||||
|
@ -30,4 +33,9 @@ interface HomeServerCapabilitiesService {
|
|||
* Get the HomeServer capabilities.
|
||||
*/
|
||||
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 response: KeysQueryResponse
|
||||
measureMetric(relevantPlugins) {
|
||||
relevantPlugins.measureMetric {
|
||||
response = try {
|
||||
downloadKeysForUsersTask.execute(params)
|
||||
} catch (throwable: Throwable) {
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
|
||||
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.HomeServerCapabilitiesService
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultHomeServerCapabilitiesService @Inject constructor(
|
||||
|
@ -33,4 +35,8 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor(
|
|||
return homeServerCapabilitiesDataSource.getHomeServerCapabilities()
|
||||
?: HomeServerCapabilities()
|
||||
}
|
||||
|
||||
override fun getHomeServerCapabilitiesLive(): LiveData<Optional<HomeServerCapabilities>> {
|
||||
return homeServerCapabilitiesDataSource.getHomeServerCapabilitiesLive()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,14 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.homeserver
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.where
|
||||
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.model.HomeServerCapabilitiesEntity
|
||||
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
|
||||
|
||||
internal class HomeServerCapabilitiesDataSource @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
) {
|
||||
fun getHomeServerCapabilities(): HomeServerCapabilities? {
|
||||
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
|
||||
|
||||
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.RuleScope
|
||||
import org.matrix.android.sdk.api.session.sync.InitialSyncStep
|
||||
|
@ -52,9 +57,12 @@ internal class SyncResponseHandler @Inject constructor(
|
|||
private val tokenStore: SyncTokenStore,
|
||||
private val processEventForPushTask: ProcessEventForPushTask,
|
||||
private val pushRuleService: PushRuleService,
|
||||
private val presenceSyncHandler: PresenceSyncHandler
|
||||
private val presenceSyncHandler: PresenceSyncHandler,
|
||||
matrixConfiguration: MatrixConfiguration,
|
||||
) {
|
||||
|
||||
private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance<SyncDurationMetricPlugin>()
|
||||
|
||||
suspend fun handleResponse(
|
||||
syncResponse: SyncResponse,
|
||||
fromToken: String?,
|
||||
|
@ -63,39 +71,91 @@ internal class SyncResponseHandler @Inject constructor(
|
|||
val isInitialSync = fromToken == null
|
||||
Timber.v("Start handling sync, is InitialSync: $isInitialSync")
|
||||
|
||||
measureTimeMillis {
|
||||
if (!cryptoService.isStarted()) {
|
||||
Timber.v("Should start cryptoService")
|
||||
cryptoService.start()
|
||||
}
|
||||
cryptoService.onSyncWillProcess(isInitialSync)
|
||||
}.also {
|
||||
Timber.v("Finish handling start cryptoService in $it ms")
|
||||
}
|
||||
relevantPlugins.measureMetric {
|
||||
startCryptoService(isInitialSync)
|
||||
|
||||
// Handle the to device events before the room ones
|
||||
// to ensure to decrypt them properly
|
||||
measureTimeMillis {
|
||||
Timber.v("Handle toDevice")
|
||||
reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) {
|
||||
if (syncResponse.toDevice != null) {
|
||||
cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
|
||||
// Handle the to device events before the room ones
|
||||
// to ensure to decrypt them properly
|
||||
handleToDevice(syncResponse, reporter)
|
||||
|
||||
val aggregator = SyncResponsePostTreatmentAggregator()
|
||||
|
||||
// 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
|
||||
// Disabled due to the new fallback
|
||||
// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
|
||||
// }
|
||||
private suspend fun handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) {
|
||||
relevantPlugins.measureSpan("task", "handle_to_device") {
|
||||
measureTimeMillis {
|
||||
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
|
||||
monarchy.awaitTransaction { realm ->
|
||||
// IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
|
||||
relevantPlugins.measureSpan("task", "monarchy_transaction") {
|
||||
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 {
|
||||
Timber.v("Handle rooms")
|
||||
reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) {
|
||||
|
@ -106,7 +166,11 @@ internal class SyncResponseHandler @Inject constructor(
|
|||
}.also {
|
||||
Timber.v("Finish handling rooms in $it ms")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAccountData(reporter: ProgressReporter?, realm: Realm, syncResponse: SyncResponse) {
|
||||
relevantPlugins.measureSpan("task", "handle_account_data") {
|
||||
measureTimeMillis {
|
||||
reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) {
|
||||
Timber.v("Handle accountData")
|
||||
|
@ -115,44 +179,59 @@ internal class SyncResponseHandler @Inject constructor(
|
|||
}.also {
|
||||
Timber.v("Finish handling accountData in $it ms")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePresence(realm: Realm, syncResponse: SyncResponse) {
|
||||
relevantPlugins.measureSpan("task", "handle_presence") {
|
||||
measureTimeMillis {
|
||||
Timber.v("Handle Presence")
|
||||
presenceSyncHandler.handle(realm, syncResponse.presence)
|
||||
}.also {
|
||||
Timber.v("Finish handling Presence in $it ms")
|
||||
}
|
||||
tokenStore.saveToken(realm, syncResponse.nextBatch)
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else we need to do outside the transaction
|
||||
measureTimeMillis {
|
||||
aggregatorHandler.handle(aggregator)
|
||||
}.also {
|
||||
Timber.v("Aggregator management took $it ms")
|
||||
}
|
||||
|
||||
measureTimeMillis {
|
||||
syncResponse.rooms?.let {
|
||||
checkPushRules(it, isInitialSync)
|
||||
userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
|
||||
dispatchInvitedRoom(it)
|
||||
private suspend fun aggregateSyncResponse(aggregator: SyncResponsePostTreatmentAggregator) {
|
||||
relevantPlugins.measureSpan("task", "aggregator_management") {
|
||||
// Everything else we need to do outside the transaction
|
||||
measureTimeMillis {
|
||||
aggregatorHandler.handle(aggregator)
|
||||
}.also {
|
||||
Timber.v("Aggregator management took $it ms")
|
||||
}
|
||||
}.also {
|
||||
Timber.v("SyncResponse.rooms post treatment took $it ms")
|
||||
}
|
||||
}
|
||||
|
||||
measureTimeMillis {
|
||||
cryptoSyncHandler.onSyncCompleted(syncResponse)
|
||||
}.also {
|
||||
Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
|
||||
private suspend fun postTreatmentSyncResponse(syncResponse: SyncResponse, isInitialSync: Boolean) {
|
||||
relevantPlugins.measureSpan("task", "sync_response_post_treatment") {
|
||||
measureTimeMillis {
|
||||
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 {
|
||||
roomSyncHandler.postSyncSpaceHierarchyHandle(it)
|
||||
}
|
||||
Timber.v("On sync completed")
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
suspend fun togglePusherForCurrentSession(enable: Boolean) {
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
val pusher = getPusherForCurrentSession() ?: return
|
||||
session.pushersService().togglePusher(pusher, enable)
|
||||
}
|
||||
|
||||
suspend fun unregisterEmailPusher(email: String) {
|
||||
val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
currentSession.pushersService().removeEmailPusher(email)
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.core.session
|
|||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
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.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
|
@ -32,6 +33,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
|
|||
private val webRtcCallManager: WebRtcCallManager,
|
||||
private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater,
|
||||
) {
|
||||
|
||||
suspend fun execute(session: Session, startSyncing: Boolean = true) {
|
||||
|
@ -46,5 +48,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
|
|||
if (vectorPreferences.isClientInfoRecordingEnabled()) {
|
||||
updateMatrixClientInfoUseCase.execute(session)
|
||||
}
|
||||
enableNotificationsSettingUpdater.onSessionsStarted(session)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.analytics.metrics
|
||||
|
||||
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 javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
@ -27,9 +28,10 @@ import javax.inject.Singleton
|
|||
@Singleton
|
||||
data class VectorPlugins @Inject constructor(
|
||||
val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics,
|
||||
val sentrySyncDurationMetrics: SentrySyncDurationMetrics,
|
||||
) {
|
||||
/**
|
||||
* 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
|
||||
|
||||
override fun startTransaction() {
|
||||
transaction = Sentry.startTransaction("download_device_keys", "task")
|
||||
logTransaction("Sentry transaction started")
|
||||
if (Sentry.isEnabled()) {
|
||||
transaction = Sentry.startTransaction("download_device_keys", "task")
|
||||
logTransaction("Sentry transaction started")
|
||||
}
|
||||
}
|
||||
|
||||
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 val timer = CountUpTimer(1000L).apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onTick(formattedDuration) }
|
||||
}
|
||||
tickListener = CountUpTimer.TickListener { milliseconds ->
|
||||
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onTick(formattedDuration) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.net.Uri
|
|||
import android.view.View
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
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.room.model.message.MessageStickerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
|
@ -129,10 +130,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
}
|
||||
|
||||
sealed class Listening : VoiceBroadcastAction() {
|
||||
data class PlayOrResume(val voiceBroadcastId: String) : Listening()
|
||||
data class PlayOrResume(val voiceBroadcast: VoiceBroadcast) : Listening()
|
||||
object Pause : 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.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(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.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() {
|
||||
amplitudeTicker?.stop()
|
||||
amplitudeTicker = CountUpTimer(50).apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
onAmplitudeTick()
|
||||
}
|
||||
}
|
||||
tickListener = CountUpTimer.TickListener { onAmplitudeTick() }
|
||||
resume()
|
||||
}
|
||||
}
|
||||
|
@ -234,11 +230,7 @@ class AudioMessageHelper @Inject constructor(
|
|||
private fun startPlaybackTicker(id: String) {
|
||||
playbackTicker?.stop()
|
||||
playbackTicker = CountUpTimer().apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
onPlaybackTick(id)
|
||||
}
|
||||
}
|
||||
tickListener = CountUpTimer.TickListener { onPlaybackTick(id) }
|
||||
resume()
|
||||
}
|
||||
onPlaybackTick(id)
|
||||
|
|
|
@ -189,11 +189,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0)
|
||||
recordingTicker?.stop()
|
||||
recordingTicker = CountUpTimer().apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
|
||||
onRecordingTick(isLocked, milliseconds + startMs)
|
||||
}
|
||||
tickListener = CountUpTimer.TickListener { milliseconds ->
|
||||
val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
|
||||
onRecordingTick(isLocked, milliseconds + startMs)
|
||||
}
|
||||
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.DrawableProvider
|
||||
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.VoiceBroadcastEventsGroup
|
||||
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.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
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.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
|
@ -44,6 +46,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
private val drawableProvider: DrawableProvider,
|
||||
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
|
||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
|
@ -58,19 +61,20 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
||||
val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: 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 &&
|
||||
voiceBroadcastEvent.root.stateKey == session.myUserId &&
|
||||
messageContent.deviceId == session.sessionParams.deviceId
|
||||
|
||||
val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
|
||||
voiceBroadcastId = voiceBroadcastId,
|
||||
voiceBroadcast = voiceBroadcast,
|
||||
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
||||
duration = voiceBroadcastEventsGroup.getDuration(),
|
||||
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
|
||||
recorder = voiceBroadcastRecorder,
|
||||
player = voiceBroadcastPlayer,
|
||||
playbackTracker = playbackTracker,
|
||||
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
||||
colorProvider = colorProvider,
|
||||
drawableProvider = drawableProvider,
|
||||
|
@ -89,7 +93,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||
): MessageVoiceBroadcastRecordingItem {
|
||||
return MessageVoiceBroadcastRecordingItem_()
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}")
|
||||
.attributes(attributes)
|
||||
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||
.highlighted(highlight)
|
||||
|
@ -102,7 +106,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||
): MessageVoiceBroadcastListeningItem {
|
||||
return MessageVoiceBroadcastListeningItem_()
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}")
|
||||
.attributes(attributes)
|
||||
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||
.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]) {
|
||||
is Listener.State.Playing -> state.percentage
|
||||
is Listener.State.Paused -> state.percentage
|
||||
|
@ -148,7 +148,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
const val RECORDING_ID = "RECORDING_ID"
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun interface Listener {
|
||||
|
||||
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.resources.ColorProvider
|
||||
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.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
@ -35,11 +37,13 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
@EpoxyAttribute
|
||||
lateinit var voiceBroadcastAttributes: Attributes
|
||||
|
||||
protected val voiceBroadcastId get() = voiceBroadcastAttributes.voiceBroadcastId
|
||||
protected val voiceBroadcast get() = voiceBroadcastAttributes.voiceBroadcast
|
||||
protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState
|
||||
protected val recorderName get() = voiceBroadcastAttributes.recorderName
|
||||
protected val recorder get() = voiceBroadcastAttributes.recorder
|
||||
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 colorProvider get() = voiceBroadcastAttributes.colorProvider
|
||||
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
||||
|
@ -92,12 +96,13 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
}
|
||||
|
||||
data class Attributes(
|
||||
val voiceBroadcastId: String,
|
||||
val voiceBroadcast: VoiceBroadcast,
|
||||
val voiceBroadcastState: VoiceBroadcastState?,
|
||||
val duration: Int,
|
||||
val recorderName: String,
|
||||
val recorder: VoiceBroadcastRecorder?,
|
||||
val player: VoiceBroadcastPlayer,
|
||||
val playbackTracker: AudioMessagePlaybackTracker,
|
||||
val roomItem: MatrixItem?,
|
||||
val colorProvider: ColorProvider,
|
||||
val drawableProvider: DrawableProvider,
|
||||
|
|
|
@ -140,16 +140,14 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
|||
}
|
||||
|
||||
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
}
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderIdleState(holder: Holder) {
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.airbnb.epoxy.EpoxyModelClass
|
|||
import im.vector.app.R
|
||||
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.timeline.helper.AudioMessagePlaybackTracker.Listener.State
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
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>() {
|
||||
|
||||
private lateinit var playerListener: VoiceBroadcastPlayer.Listener
|
||||
private var isUserSeeking = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
@ -41,11 +43,35 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
}
|
||||
|
||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||
playerListener = VoiceBroadcastPlayer.Listener { state ->
|
||||
renderPlayingState(holder, state)
|
||||
}
|
||||
player.addListener(voiceBroadcastId, playerListener)
|
||||
playerListener = VoiceBroadcastPlayer.Listener { renderPlayingState(holder, it) }
|
||||
player.addListener(voiceBroadcast, playerListener)
|
||||
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) {
|
||||
|
@ -61,50 +87,67 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
||||
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
||||
|
||||
fastBackwardButton.isInvisible = true
|
||||
fastForwardButton.isInvisible = true
|
||||
|
||||
when (state) {
|
||||
VoiceBroadcastPlayer.State.PLAYING -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
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.PAUSED -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
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) {
|
||||
holder.durationView.text = formatPlaybackTime(voiceBroadcastAttributes.duration)
|
||||
holder.seekBar.max = voiceBroadcastAttributes.duration
|
||||
holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit
|
||||
with(holder) {
|
||||
durationView.text = formatPlaybackTime(duration)
|
||||
seekBar.max = duration
|
||||
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) {
|
||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress))
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
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())
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
super.unbind(holder)
|
||||
player.removeListener(voiceBroadcastId, playerListener)
|
||||
holder.seekBar.setOnSeekBarChangeListener(null)
|
||||
player.removeListener(voiceBroadcast, playerListener)
|
||||
playbackTracker.untrack(voiceBroadcast.voiceBroadcastId)
|
||||
with(holder) {
|
||||
seekBar.onClick(null)
|
||||
playPauseButton.onClick(null)
|
||||
fastForwardButton.onClick(null)
|
||||
fastBackwardButton.onClick(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
|
|
@ -122,16 +122,14 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
true
|
||||
}
|
||||
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, 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.Recording -> Unit
|
||||
}
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, 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.Recording -> Unit
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
|
||||
}
|
||||
holder.timer.tickListener = CountUpTimer.TickListener {
|
||||
holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
|
||||
}
|
||||
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
|
||||
|
||||
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 javax.inject.Inject
|
||||
|
||||
class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() {
|
||||
|
||||
fun execute(deviceId: String): Boolean {
|
||||
return activeSessionHolder
|
||||
.getSafeActiveSession()
|
||||
?.accountDataService()
|
||||
?.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null
|
||||
fun execute(session: Session, deviceId: String): Boolean {
|
||||
return session
|
||||
.accountDataService()
|
||||
.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,20 +16,15 @@
|
|||
|
||||
package im.vector.app.features.settings.devices.v2.notification
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import javax.inject.Inject
|
||||
|
||||
class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() {
|
||||
|
||||
fun execute(): Boolean {
|
||||
return activeSessionHolder
|
||||
.getSafeActiveSession()
|
||||
?.homeServerCapabilitiesService()
|
||||
?.getHomeServerCapabilities()
|
||||
?.canRemotelyTogglePushNotificationsOfDevices
|
||||
.orFalse()
|
||||
fun execute(session: Session): Boolean {
|
||||
return session
|
||||
.homeServerCapabilitiesService()
|
||||
.getHomeServerCapabilities()
|
||||
.canRemotelyTogglePushNotificationsOfDevices
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,13 @@
|
|||
|
||||
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.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
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.events.model.toModel
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
|
@ -29,16 +30,13 @@ import org.matrix.android.sdk.flow.unwrap
|
|||
import javax.inject.Inject
|
||||
|
||||
class GetNotificationsStatusUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
|
||||
private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase,
|
||||
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
|
||||
) {
|
||||
|
||||
fun execute(deviceId: String): Flow<NotificationsStatus> {
|
||||
val session = activeSessionHolder.getSafeActiveSession()
|
||||
fun execute(session: Session, deviceId: String): Flow<NotificationsStatus> {
|
||||
return when {
|
||||
session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED)
|
||||
checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> {
|
||||
checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> {
|
||||
session.flow()
|
||||
.liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
|
||||
.unwrap()
|
||||
|
@ -46,15 +44,19 @@ class GetNotificationsStatusUseCase @Inject constructor(
|
|||
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> {
|
||||
session.flow()
|
||||
.livePushers()
|
||||
.map { it.filter { pusher -> pusher.deviceId == deviceId } }
|
||||
.map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
|
||||
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
else -> flowOf(NotificationsStatus.NOT_SUPPORTED)
|
||||
else -> canTogglePushNotificationsViaPusherUseCase.execute(session)
|
||||
.flatMapLatest { canToggle ->
|
||||
if (canToggle) {
|
||||
session.flow()
|
||||
.livePushers()
|
||||
.map { it.filter { pusher -> pusher.deviceId == deviceId } }
|
||||
.map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
|
||||
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
|
||||
.distinctUntilChanged()
|
||||
} else {
|
||||
flowOf(NotificationsStatus.NOT_SUPPORTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,14 +31,14 @@ class TogglePushNotificationUseCase @Inject constructor(
|
|||
suspend fun execute(deviceId: String, enabled: Boolean) {
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
|
||||
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) {
|
||||
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
|
||||
val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
|
||||
devicePusher?.let { pusher ->
|
||||
session.pushersService().togglePusher(pusher, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId)) {
|
||||
if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) {
|
||||
val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
|
||||
session.accountDataService().updateUserAccountData(
|
||||
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)
|
||||
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 -> {
|
||||
views.otherSessionsSecurityRecommendationView.render(
|
||||
|
|
|
@ -300,7 +300,7 @@ class SessionOverviewFragment :
|
|||
R.string.device_manager_verification_status_unverified
|
||||
}
|
||||
val descriptionResId = if (isVerified) {
|
||||
R.string.device_manager_learn_more_sessions_verified
|
||||
R.string.device_manager_learn_more_sessions_verified_description
|
||||
} else {
|
||||
R.string.device_manager_learn_more_sessions_unverified
|
||||
}
|
||||
|
|
|
@ -110,9 +110,11 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun observeNotificationsStatus(deviceId: String) {
|
||||
getNotificationsStatusUseCase.execute(deviceId)
|
||||
.onEach { setState { copy(notificationsStatus = it) } }
|
||||
.launchIn(viewModelScope)
|
||||
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||
getNotificationsStatusUseCase.execute(session, deviceId)
|
||||
.onEach { setState { copy(notificationsStatus = it) } }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
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.lib.core.utils.compat.getParcelableExtraCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -81,6 +80,8 @@ class VectorSettingsNotificationPreferenceFragment :
|
|||
@Inject lateinit var guardServiceStarter: GuardServiceStarter
|
||||
@Inject lateinit var vectorFeatures: VectorFeatures
|
||||
@Inject lateinit var notificationPermissionManager: NotificationPermissionManager
|
||||
@Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase
|
||||
@Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase
|
||||
|
||||
override var titleRes: Int = R.string.settings_notifications
|
||||
override val preferenceXmlRes = R.xml.vector_settings_notifications
|
||||
|
@ -119,48 +120,25 @@ class VectorSettingsNotificationPreferenceFragment :
|
|||
(pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel
|
||||
}
|
||||
|
||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let {
|
||||
pushersManager.getPusherForCurrentSession()?.let { pusher ->
|
||||
it.isChecked = pusher.enabled
|
||||
}
|
||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
|
||||
?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
|
||||
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)
|
||||
?.summary = unifiedPushHelper.getCurrentDistributorName()
|
||||
lifecycleScope.launch {
|
||||
val result = runCatching {
|
||||
pushersManager.togglePusherForCurrentSession(true)
|
||||
}
|
||||
|
||||
result.exceptionOrNull()?.let { _ ->
|
||||
Toast.makeText(context, R.string.error_check_network, Toast.LENGTH_SHORT).show()
|
||||
it.isChecked = false
|
||||
}
|
||||
}
|
||||
notificationPermissionManager.eventuallyRequestPermission(
|
||||
requireActivity(),
|
||||
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 {
|
||||
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
|
||||
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.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.getRelationContent
|
||||
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.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
|
||||
|
||||
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.ResumeVoiceBroadcastUseCase
|
||||
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)
|
||||
|
||||
fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId)
|
||||
fun playOrResumePlayback(voiceBroadcast: VoiceBroadcast) = voiceBroadcastPlayer.playOrResume(voiceBroadcast)
|
||||
|
||||
fun pausePlayback() = voiceBroadcastPlayer.pause()
|
||||
|
||||
fun stopPlayback() = voiceBroadcastPlayer.stop()
|
||||
|
||||
fun seekTo(voiceBroadcastId: String, positionMillis: Int) {
|
||||
if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) {
|
||||
voiceBroadcastPlayer.seekTo(positionMillis)
|
||||
}
|
||||
fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) {
|
||||
voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis, duration)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
|
||||
package im.vector.app.features.voicebroadcast.listening
|
||||
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
|
||||
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.
|
||||
|
@ -31,7 +33,7 @@ interface VoiceBroadcastPlayer {
|
|||
/**
|
||||
* 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.
|
||||
|
@ -44,19 +46,19 @@ interface VoiceBroadcastPlayer {
|
|||
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.
|
||||
|
|
|
@ -18,28 +18,28 @@ package im.vector.app.features.voicebroadcast.listening
|
|||
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.media.MediaPlayer.OnPreparedListener
|
||||
import androidx.annotation.MainThread
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
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.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.State
|
||||
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.usecase.GetVoiceBroadcastUseCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
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.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.inject.Inject
|
||||
|
@ -49,179 +49,161 @@ import javax.inject.Singleton
|
|||
class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||
private val sessionHolder: ActiveSessionHolder,
|
||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
||||
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
|
||||
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
|
||||
) : VoiceBroadcastPlayer {
|
||||
|
||||
private val session
|
||||
get() = sessionHolder.getActiveSession()
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private var voiceBroadcastStateJob: Job? = null
|
||||
private val session get() = sessionHolder.getActiveSession()
|
||||
private val sessionScope get() = session.coroutineScope
|
||||
|
||||
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 nextMediaPlayer: MediaPlayer? = null
|
||||
private var currentSequence: Int? = null
|
||||
private var isPreparingNextPlayer: Boolean = false
|
||||
|
||||
private var fetchPlaylistJob: Job? = null
|
||||
private var playlist = emptyList<PlaylistItem>()
|
||||
private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
|
||||
|
||||
private var isLive: Boolean = false
|
||||
|
||||
override var currentVoiceBroadcastId: String? = null
|
||||
override var currentVoiceBroadcast: VoiceBroadcast? = null
|
||||
|
||||
override var playingState = State.IDLE
|
||||
@MainThread
|
||||
set(value) {
|
||||
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
||||
field = value
|
||||
// Notify state change to all the listeners attached to the current voice broadcast id
|
||||
currentVoiceBroadcastId?.let { voiceBroadcastId ->
|
||||
listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) }
|
||||
if (field != value) {
|
||||
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
||||
field = value
|
||||
onPlayingStateChanged(value)
|
||||
}
|
||||
}
|
||||
private var currentRoomId: String? = null
|
||||
|
||||
/**
|
||||
* Map voiceBroadcastId to listeners.
|
||||
*/
|
||||
/** Map voiceBroadcastId to listeners.*/
|
||||
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
|
||||
|
||||
override fun playOrResume(roomId: String, voiceBroadcastId: String) {
|
||||
val hasChanged = currentVoiceBroadcastId != voiceBroadcastId
|
||||
override fun playOrResume(voiceBroadcast: VoiceBroadcast) {
|
||||
val hasChanged = currentVoiceBroadcast != voiceBroadcast
|
||||
when {
|
||||
hasChanged -> startPlayback(roomId, voiceBroadcastId)
|
||||
hasChanged -> startPlayback(voiceBroadcast)
|
||||
playingState == State.PAUSED -> resumePlayback()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
currentMediaPlayer?.pause()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
|
||||
playingState = State.PAUSED
|
||||
pausePlayback()
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
playlist = emptyList()
|
||||
currentSequence = null
|
||||
playlist.reset()
|
||||
|
||||
currentRoomId = null
|
||||
currentVoiceBroadcastId = null
|
||||
currentVoiceBroadcastEvent = null
|
||||
currentVoiceBroadcast = null
|
||||
}
|
||||
|
||||
override fun addListener(voiceBroadcastId: String, listener: Listener) {
|
||||
listeners[voiceBroadcastId]?.add(listener) ?: run {
|
||||
listeners[voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||
override fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
|
||||
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
|
||||
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) {
|
||||
listeners[voiceBroadcastId]?.remove(listener)
|
||||
override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: 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
|
||||
if (playingState != State.IDLE) stop()
|
||||
|
||||
currentRoomId = roomId
|
||||
currentVoiceBroadcastId = eventId
|
||||
currentVoiceBroadcast = voiceBroadcast
|
||||
|
||||
playingState = State.BUFFERING
|
||||
|
||||
val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
|
||||
isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
|
||||
fetchPlaylistAndStartPlayback(roomId, eventId)
|
||||
observeVoiceBroadcastLiveState(voiceBroadcast)
|
||||
fetchPlaylistAndStartPlayback(voiceBroadcast)
|
||||
}
|
||||
|
||||
private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) {
|
||||
fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId)
|
||||
.onEach(this::updatePlaylist)
|
||||
.launchIn(coroutineScope)
|
||||
private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) {
|
||||
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
|
||||
.onEach { currentVoiceBroadcastEvent = it.getOrNull() }
|
||||
.launchIn(sessionScope)
|
||||
}
|
||||
|
||||
private fun updatePlaylist(audioEvents: List<MessageAudioEvent>) {
|
||||
val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs }
|
||||
val chunkPositions = sorted
|
||||
.map { it.duration }
|
||||
.runningFold(0) { acc, i -> acc + i }
|
||||
.dropLast(1)
|
||||
playlist = sorted.mapIndexed { index, messageAudioEvent ->
|
||||
PlaylistItem(
|
||||
audioEvent = messageAudioEvent,
|
||||
startTime = chunkPositions.getOrNull(index) ?: 0
|
||||
)
|
||||
}
|
||||
onPlaylistUpdated()
|
||||
private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
|
||||
fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
|
||||
.onEach {
|
||||
playlist.setItems(it)
|
||||
onPlaylistUpdated()
|
||||
}
|
||||
.launchIn(sessionScope)
|
||||
}
|
||||
|
||||
private fun onPlaylistUpdated() {
|
||||
when (playingState) {
|
||||
State.PLAYING -> {
|
||||
if (nextMediaPlayer == null) {
|
||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
||||
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
State.PAUSED -> {
|
||||
if (nextMediaPlayer == null) {
|
||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
||||
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
State.BUFFERING -> {
|
||||
val newMediaContent = getNextAudioContent()
|
||||
if (newMediaContent != null) startPlayback()
|
||||
val nextItem = playlist.getNextItem()
|
||||
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 {
|
||||
sequence != null -> playlist.find { it.audioEvent.sequence == sequence }
|
||||
isLive -> playlist.lastOrNull()
|
||||
position != null -> playlist.findByPosition(position)
|
||||
currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
|
||||
else -> playlist.firstOrNull()
|
||||
}
|
||||
val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
||||
val computedSequence = playlistItem.audioEvent.sequence
|
||||
coroutineScope.launch {
|
||||
val sequence = playlistItem.audioEvent.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return }
|
||||
val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0
|
||||
sessionScope.launch {
|
||||
try {
|
||||
currentMediaPlayer = prepareMediaPlayer(content)
|
||||
currentMediaPlayer?.start()
|
||||
if (position > 0) {
|
||||
currentMediaPlayer?.seekTo(position)
|
||||
prepareMediaPlayer(content) { mp ->
|
||||
currentMediaPlayer = mp
|
||||
playlist.currentSequence = sequence
|
||||
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) {
|
||||
Timber.e(failure, "Unable to start playback")
|
||||
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() {
|
||||
currentMediaPlayer?.start()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
||||
playingState = State.PLAYING
|
||||
if (currentMediaPlayer != null) {
|
||||
currentMediaPlayer?.start()
|
||||
playingState = State.PLAYING
|
||||
} else {
|
||||
val position = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
|
||||
startPlayback(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekTo(positionMillis: Int) {
|
||||
val duration = getVoiceBroadcastDuration()
|
||||
val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return
|
||||
val audioEvent = playlistItem.audioEvent
|
||||
val eventPosition = positionMillis - playlistItem.startTime
|
||||
|
||||
Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${audioEvent.sequence}, sequencePosition=$eventPosition")
|
||||
|
||||
tryOrNull { currentMediaPlayer?.stop() }
|
||||
release(currentMediaPlayer)
|
||||
tryOrNull { nextMediaPlayer?.stop() }
|
||||
release(nextMediaPlayer)
|
||||
|
||||
startPlayback(audioEvent.sequence, eventPosition)
|
||||
override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) {
|
||||
when {
|
||||
voiceBroadcast != currentVoiceBroadcast -> {
|
||||
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||
}
|
||||
playingState == State.PLAYING || playingState == State.BUFFERING -> {
|
||||
startPlayback(positionMillis)
|
||||
}
|
||||
playingState == State.IDLE || playingState == State.PAUSED -> {
|
||||
pausePlayback(positionMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNextAudioContent(): MessageAudioContent? {
|
||||
val nextSequence = currentSequence?.plus(1)
|
||||
?: playlist.lastOrNull()?.audioEvent?.sequence
|
||||
?: 1
|
||||
return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content
|
||||
private fun prepareNextMediaPlayer() {
|
||||
val nextItem = playlist.getNextItem()
|
||||
if (nextItem != null) {
|
||||
isPreparingNextPlayer = true
|
||||
sessionScope.launch {
|
||||
prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
|
||||
nextMediaPlayer = mp
|
||||
currentMediaPlayer?.setNextMediaPlayer(mp)
|
||||
isPreparingNextPlayer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareNextMediaPlayer(): MediaPlayer? {
|
||||
val nextContent = getNextAudioContent() ?: return null
|
||||
return prepareMediaPlayer(nextContent)
|
||||
}
|
||||
|
||||
private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent): MediaPlayer {
|
||||
private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent, onPreparedListener: OnPreparedListener): MediaPlayer {
|
||||
// Download can fail
|
||||
val audioFile = try {
|
||||
session.fileService().downloadFile(messageAudioContent)
|
||||
|
@ -284,58 +284,76 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
setDataSource(fis.fd)
|
||||
setOnInfoListener(mediaPlayerListener)
|
||||
setOnErrorListener(mediaPlayerListener)
|
||||
setOnPreparedListener(onPreparedListener)
|
||||
setOnCompletionListener(mediaPlayerListener)
|
||||
prepare()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun release(mp: MediaPlayer?) {
|
||||
mp?.apply {
|
||||
release()
|
||||
setOnInfoListener(null)
|
||||
setOnCompletionListener(null)
|
||||
setOnErrorListener(null)
|
||||
private fun stopPlayer() {
|
||||
tryOrNull { currentMediaPlayer?.stop() }
|
||||
currentMediaPlayer?.release()
|
||||
currentMediaPlayer = 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 :
|
||||
MediaPlayer.OnInfoListener,
|
||||
MediaPlayer.OnPreparedListener,
|
||||
MediaPlayer.OnCompletionListener,
|
||||
MediaPlayer.OnErrorListener {
|
||||
|
||||
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||
when (what) {
|
||||
MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
|
||||
release(currentMediaPlayer)
|
||||
playlist.currentSequence = playlist.currentSequence?.inc()
|
||||
currentMediaPlayer = mp
|
||||
currentSequence = currentSequence?.plus(1)
|
||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
||||
nextMediaPlayer = null
|
||||
playingState = State.PLAYING
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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
|
||||
stop()
|
||||
} 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.features.voicebroadcast.getVoiceBroadcastEventId
|
||||
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.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
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.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
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.room.model.message.MessageAudioEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||
|
@ -44,19 +47,19 @@ import javax.inject.Inject
|
|||
*/
|
||||
class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
||||
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 room = session.roomService().getRoom(roomId) ?: return emptyFlow()
|
||||
val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow()
|
||||
val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
|
||||
|
||||
// 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() } }
|
||||
|
||||
val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)
|
||||
val voiceBroadcastEvent = runBlocking { getVoiceBroadcastEventUseCase.execute(voiceBroadcast).firstOrNull()?.getOrNull() }
|
||||
val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
|
||||
|
||||
return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) {
|
||||
|
@ -82,7 +85,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
|||
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
|
||||
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:layout_width="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:src="@drawable/ic_player_backward_30"
|
||||
app:tint="?vctr_content_secondary" />
|
||||
android:visibility="invisible"
|
||||
app:tint="?vctr_content_secondary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playPauseButton"
|
||||
|
@ -121,16 +123,20 @@
|
|||
android:layout_height="@dimen/voice_broadcast_player_button_size"
|
||||
android:contentDescription="@string/a11y_voice_broadcast_buffering"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="?vctr_content_secondary" />
|
||||
android:indeterminateTint="?vctr_content_secondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/fastForwardButton"
|
||||
android:layout_width="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:src="@drawable/ic_player_forward_30"
|
||||
app:tint="?vctr_content_secondary" />
|
||||
android:visibility="invisible"
|
||||
app:tint="?vctr_content_secondary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<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.SessionParamsFixture
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
|
||||
|
@ -101,19 +100,4 @@ class PushersManagerTest {
|
|||
|
||||
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.session.clientinfo.UpdateMatrixClientInfoUseCase
|
||||
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.FakeVectorPreferences
|
||||
import im.vector.app.test.fakes.FakeWebRtcCallManager
|
||||
|
@ -43,12 +44,14 @@ class ConfigureAndStartSessionUseCaseTest {
|
|||
private val fakeWebRtcCallManager = FakeWebRtcCallManager()
|
||||
private val fakeUpdateMatrixClientInfoUseCase = mockk<UpdateMatrixClientInfoUseCase>()
|
||||
private val fakeVectorPreferences = FakeVectorPreferences()
|
||||
private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater()
|
||||
|
||||
private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase(
|
||||
context = fakeContext.instance,
|
||||
webRtcCallManager = fakeWebRtcCallManager.instance,
|
||||
updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase,
|
||||
vectorPreferences = fakeVectorPreferences.instance,
|
||||
enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance,
|
||||
)
|
||||
|
||||
@Before
|
||||
|
@ -68,6 +71,7 @@ class ConfigureAndStartSessionUseCaseTest {
|
|||
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
||||
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
||||
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
|
||||
fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
|
||||
|
||||
// When
|
||||
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
|
||||
|
@ -87,6 +91,7 @@ class ConfigureAndStartSessionUseCaseTest {
|
|||
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
||||
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
||||
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false)
|
||||
fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
|
||||
|
||||
// When
|
||||
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
|
||||
|
@ -106,6 +111,7 @@ class ConfigureAndStartSessionUseCaseTest {
|
|||
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
|
||||
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
|
||||
fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
|
||||
fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
|
||||
|
||||
// When
|
||||
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
|
||||
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
import io.mockk.mockk
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
|
@ -26,18 +26,15 @@ private const val A_DEVICE_ID = "device-id"
|
|||
|
||||
class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
||||
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
private val fakeSession = FakeSession()
|
||||
|
||||
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase =
|
||||
CheckIfCanTogglePushNotificationsViaAccountDataUseCase(
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||
)
|
||||
CheckIfCanTogglePushNotificationsViaAccountDataUseCase()
|
||||
|
||||
@Test
|
||||
fun `given current session and an account data for the device id when execute then result is true`() {
|
||||
// Given
|
||||
fakeActiveSessionHolder
|
||||
.fakeSession
|
||||
fakeSession
|
||||
.accountDataService()
|
||||
.givenGetUserAccountDataEventReturns(
|
||||
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
||||
|
@ -45,7 +42,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
|||
)
|
||||
|
||||
// When
|
||||
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
|
||||
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||
|
||||
// Then
|
||||
result shouldBeEqualTo true
|
||||
|
@ -54,8 +51,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
|||
@Test
|
||||
fun `given current session and NO account data for the device id when execute then result is false`() {
|
||||
// Given
|
||||
fakeActiveSessionHolder
|
||||
.fakeSession
|
||||
fakeSession
|
||||
.accountDataService()
|
||||
.givenGetUserAccountDataEventReturns(
|
||||
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
||||
|
@ -63,7 +59,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
|
|||
)
|
||||
|
||||
// When
|
||||
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
|
||||
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||
|
||||
// Then
|
||||
result shouldBeEqualTo false
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
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 org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
|
@ -25,37 +25,22 @@ private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyToggl
|
|||
|
||||
class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest {
|
||||
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
private val fakeSession = FakeSession()
|
||||
|
||||
private val checkIfCanTogglePushNotificationsViaPusherUseCase =
|
||||
CheckIfCanTogglePushNotificationsViaPusherUseCase(
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||
)
|
||||
CheckIfCanTogglePushNotificationsViaPusherUseCase()
|
||||
|
||||
@Test
|
||||
fun `given current session when execute then toggle capability is returned`() {
|
||||
// Given
|
||||
fakeActiveSessionHolder
|
||||
.fakeSession
|
||||
fakeSession
|
||||
.fakeHomeServerCapabilitiesService
|
||||
.givenCapabilities(A_HOMESERVER_CAPABILITIES)
|
||||
|
||||
// When
|
||||
val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute()
|
||||
val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession)
|
||||
|
||||
// Then
|
||||
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
|
||||
|
||||
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.testDispatcher
|
||||
import io.mockk.every
|
||||
|
@ -25,6 +25,7 @@ import io.mockk.mockk
|
|||
import io.mockk.verifyOrder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
|
@ -44,17 +45,16 @@ class GetNotificationsStatusUseCaseTest {
|
|||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase =
|
||||
mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
|
||||
private val fakeSession = FakeSession()
|
||||
private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
|
||||
mockk<CheckIfCanTogglePushNotificationsViaAccountDataUseCase>()
|
||||
private val fakeCanTogglePushNotificationsViaPusherUseCase =
|
||||
mockk<CanTogglePushNotificationsViaPusherUseCase>()
|
||||
|
||||
private val getNotificationsStatusUseCase =
|
||||
GetNotificationsStatusUseCase(
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||
checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
|
||||
checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase,
|
||||
canTogglePushNotificationsViaPusherUseCase = fakeCanTogglePushNotificationsViaPusherUseCase,
|
||||
)
|
||||
|
||||
@Before
|
||||
|
@ -67,33 +67,21 @@ class GetNotificationsStatusUseCaseTest {
|
|||
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
|
||||
fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
|
||||
// Given
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
|
||||
every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
|
||||
|
||||
// When
|
||||
val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID)
|
||||
val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||
|
||||
// Then
|
||||
result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED
|
||||
verifyOrder {
|
||||
// we should first check account data
|
||||
fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
|
||||
fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute()
|
||||
fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||
fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,12 +94,12 @@ class GetNotificationsStatusUseCaseTest {
|
|||
enabled = true,
|
||||
)
|
||||
)
|
||||
fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false
|
||||
fakeSession.pushersService().givenPushersLive(pushers)
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
|
||||
every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true)
|
||||
|
||||
// When
|
||||
val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID)
|
||||
val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||
|
||||
// Then
|
||||
result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
|
||||
|
@ -120,8 +108,7 @@ class GetNotificationsStatusUseCaseTest {
|
|||
@Test
|
||||
fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest {
|
||||
// Given
|
||||
fakeActiveSessionHolder
|
||||
.fakeSession
|
||||
fakeSession
|
||||
.accountDataService()
|
||||
.givenGetUserAccountDataEventReturns(
|
||||
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
|
||||
|
@ -129,11 +116,11 @@ class GetNotificationsStatusUseCaseTest {
|
|||
isSilenced = false
|
||||
).toContent(),
|
||||
)
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns true
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true
|
||||
every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
|
||||
|
||||
// When
|
||||
val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID)
|
||||
val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
|
||||
|
||||
// Then
|
||||
result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
|
||||
|
|
|
@ -49,10 +49,11 @@ class TogglePushNotificationUseCaseTest {
|
|||
PusherFixture.aPusher(deviceId = sessionId, enabled = false),
|
||||
PusherFixture.aPusher(deviceId = "another id", enabled = false)
|
||||
)
|
||||
activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
|
||||
activeSessionHolder.fakeSession.pushersService().givenGetPushers(pushers)
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns false
|
||||
val fakeSession = activeSessionHolder.fakeSession
|
||||
fakeSession.pushersService().givenPushersLive(pushers)
|
||||
fakeSession.pushersService().givenGetPushers(pushers)
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false
|
||||
|
||||
// When
|
||||
togglePushNotificationUseCase.execute(sessionId, true)
|
||||
|
@ -69,13 +70,14 @@ class TogglePushNotificationUseCaseTest {
|
|||
PusherFixture.aPusher(deviceId = sessionId, enabled = false),
|
||||
PusherFixture.aPusher(deviceId = "another id", enabled = false)
|
||||
)
|
||||
activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
|
||||
activeSessionHolder.fakeSession.accountDataService().givenGetUserAccountDataEventReturns(
|
||||
val fakeSession = activeSessionHolder.fakeSession
|
||||
fakeSession.pushersService().givenPushersLive(pushers)
|
||||
fakeSession.accountDataService().givenGetUserAccountDataEventReturns(
|
||||
UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
|
||||
LocalNotificationSettingsContent(isSilenced = true).toContent()
|
||||
)
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns true
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
|
||||
every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true
|
||||
|
||||
// When
|
||||
togglePushNotificationUseCase.execute(sessionId, true)
|
||||
|
|
|
@ -22,11 +22,11 @@ import com.airbnb.mvrx.Success
|
|||
import com.airbnb.mvrx.test.MavericksTestRule
|
||||
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.notification.GetNotificationsStatusUseCase
|
||||
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.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||
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.FakeSharedPreferences
|
||||
import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
|
||||
|
@ -76,7 +76,7 @@ class SessionOverviewViewModelTest {
|
|||
private val fakePendingAuthHandler = FakePendingAuthHandler()
|
||||
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
|
||||
private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase()
|
||||
private val fakeGetNotificationsStatusUseCase = mockk<GetNotificationsStatusUseCase>()
|
||||
private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase()
|
||||
private val notificationsStatus = NotificationsStatus.ENABLED
|
||||
private val fakeSharedPreferences = FakeSharedPreferences()
|
||||
|
||||
|
@ -90,7 +90,7 @@ class SessionOverviewViewModelTest {
|
|||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||
refreshDevicesUseCase = refreshDevicesUseCase,
|
||||
togglePushNotificationUseCase = togglePushNotificationUseCase.instance,
|
||||
getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase,
|
||||
getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance,
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
)
|
||||
|
||||
|
@ -101,7 +101,11 @@ class SessionOverviewViewModelTest {
|
|||
every { SystemClock.elapsedRealtime() } returns 1234
|
||||
|
||||
givenVerificationService()
|
||||
every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus)
|
||||
fakeGetNotificationsStatusUseCase.givenExecuteReturns(
|
||||
fakeActiveSessionHolder.fakeSession,
|
||||
A_SESSION_ID_1,
|
||||
notificationsStatus
|
||||
)
|
||||
fakeSharedPreferences.givenSessionManagerShowIpAddress(false)
|
||||
}
|
||||
|
||||
|
@ -416,13 +420,10 @@ class SessionOverviewViewModelTest {
|
|||
|
||||
@Test
|
||||
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()
|
||||
|
||||
viewModel.test()
|
||||
.assertLatestState { state -> state.notificationsStatus == notificationStatus }
|
||||
.assertLatestState { state -> state.notificationsStatus == notificationsStatus }
|
||||
.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
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
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() {
|
||||
|
||||
fun givenCapabilities(homeServerCapabilities: 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 io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
||||
|
@ -42,5 +43,13 @@ class FakeVectorPreferences {
|
|||
}
|
||||
|
||||
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…
Add table
Reference in a new issue