Merge branch 'develop' into feature/fga/avoid_deadlocks

This commit is contained in:
ganfra 2023-07-25 16:09:24 +02:00
commit cfd43af45c
307 changed files with 2806 additions and 1172 deletions

View file

@ -59,7 +59,7 @@ fun Context.isAnimationEnabled(): Boolean {
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
/**
* Return the application label of the provided package. If not found, the package is returned.

View file

@ -138,7 +138,7 @@ fun Modifier.shapeShadow(
val paint = Paint()
val frameworkPaint = paint.asFrameworkPaint()
if (blurRadius != 0.dp) {
frameworkPaint.maskFilter = (BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL))
frameworkPaint.maskFilter = BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL)
}
frameworkPaint.color = color.toArgb()

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.preview
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
@OptIn(ExperimentalMaterial3Api::class)
val sheetStateForPreview = SheetState(
skipPartiallyExpanded = true,
initialValue = SheetValue.Expanded,
)

View file

@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@ -38,6 +37,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.preview.sheetStateForPreview
import io.element.android.libraries.theme.ElementTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -100,10 +100,7 @@ private fun ContentToPreview() {
) {
ModalBottomSheet(
onDismissRequest = {},
sheetState = SheetState(
skipPartiallyExpanded = true,
initialValue = SheetValue.Expanded,
),
sheetState = sheetStateForPreview,
) {
Text(
text = "Sheet Content",

View file

@ -30,7 +30,7 @@ fun LogCompositions(tag: String, msg: String) {
if (BuildConfig.DEBUG) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
Timber.d(tag, "Compositions: $msg ${ref.value}")
Timber.tag(tag).d("Compositions: $msg ${ref.value}")
}
}

View file

@ -22,16 +22,8 @@ enum class FeatureFlags(
override val description: String? = null,
override val defaultValue: Boolean = true
) : Feature {
CollapseRoomStateEvents(
key = "feature.collapseroomstateevents",
title = "Collapse room state events",
),
ShowStartChatFlow(
key = "feature.showstartchatflow",
title = "Show start chat flow",
),
ShowMediaUploadingFlow(
key = "feature.showmediauploadingflow",
title = "Show media uploading flow",
LocationSharing(
key = "feature.locationsharing",
title = "Allow user to share location",
)
}

View file

@ -29,9 +29,7 @@ class BuildtimeFeatureFlagProvider @Inject constructor() :
override suspend fun isFeatureEnabled(feature: Feature): Boolean {
return if (feature is FeatureFlags) {
when (feature) {
FeatureFlags.CollapseRoomStateEvents -> false
FeatureFlags.ShowStartChatFlow -> false
FeatureFlags.ShowMediaUploadingFlow -> false
FeatureFlags.LocationSharing -> true
}
} else {
false

View file

@ -26,14 +26,14 @@ class DefaultFeatureFlagServiceTest {
@Test
fun `given service without provider when feature is checked then it returns the default value`() = runTest {
val featureFlagService = DefaultFeatureFlagService(emptySet())
val isFeatureEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)
assertThat(isFeatureEnabled).isEqualTo(FeatureFlags.ShowStartChatFlow.defaultValue)
val isFeatureEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)
assertThat(isFeatureEnabled).isEqualTo(FeatureFlags.LocationSharing.defaultValue)
}
@Test
fun `given service without provider when set enabled feature is called then it returns false`() = runTest {
val featureFlagService = DefaultFeatureFlagService(emptySet())
val result = featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true)
val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
assertThat(result).isEqualTo(false)
}
@ -41,7 +41,7 @@ class DefaultFeatureFlagServiceTest {
fun `given service with a runtime provider when set enabled feature is called then it returns true`() = runTest {
val featureFlagProvider = FakeRuntimeFeatureFlagProvider(0)
val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider))
val result = featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true)
val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
assertThat(result).isEqualTo(true)
}
@ -49,10 +49,10 @@ class DefaultFeatureFlagServiceTest {
fun `given service with a runtime provider and feature enabled when feature is checked then it returns the correct value`() = runTest {
val featureFlagProvider = FakeRuntimeFeatureFlagProvider(0)
val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider))
featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true)
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(true)
featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, false)
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(false)
featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true)
featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, false)
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(false)
}
@Test
@ -60,8 +60,8 @@ class DefaultFeatureFlagServiceTest {
val lowPriorityfeatureFlagProvider = FakeRuntimeFeatureFlagProvider(LOW_PRIORITY)
val highPriorityfeatureFlagProvider = FakeRuntimeFeatureFlagProvider(HIGH_PRIORITY)
val featureFlagService = DefaultFeatureFlagService(setOf(lowPriorityfeatureFlagProvider, highPriorityfeatureFlagProvider))
lowPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, false)
highPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true)
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(true)
lowPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, false)
highPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, true)
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true)
}
}

View file

@ -18,6 +18,8 @@
package io.element.android.libraries.maplibre.compose
import androidx.compose.ui.graphics.Color
internal val DefaultMapLocationSettings = MapLocationSettings()
/**
@ -28,4 +30,11 @@ internal val DefaultMapLocationSettings = MapLocationSettings()
*/
public data class MapLocationSettings(
public val locationEnabled: Boolean = false,
public val backgroundTintColor: Color = Color.Unspecified,
public val foregroundTintColor: Color = Color.Unspecified,
public val backgroundStaleTintColor: Color = Color.Unspecified,
public val foregroundStaleTintColor: Color = Color.Unspecified,
public val accuracyColor: Color = Color.Unspecified,
public val pulseEnabled: Boolean = false,
public val pulseColor: Color = Color.Unspecified
)

View file

@ -39,6 +39,7 @@ internal class MapPropertiesNode(
style: Style,
context: Context,
cameraPositionState: CameraPositionState,
locationSettings: MapLocationSettings,
) : MapNode {
init {
@ -46,7 +47,13 @@ internal class MapPropertiesNode(
LocationComponentActivationOptions.Builder(context, style)
.locationComponentOptions(
LocationComponentOptions.builder(context)
.pulseEnabled(true)
.backgroundTintColor(locationSettings.backgroundTintColor.toArgb())
.foregroundTintColor(locationSettings.foregroundTintColor.toArgb())
.backgroundStaleTintColor(locationSettings.backgroundStaleTintColor.toArgb())
.foregroundStaleTintColor(locationSettings.foregroundStaleTintColor.toArgb())
.accuracyColor(locationSettings.accuracyColor.toArgb())
.pulseEnabled(locationSettings.pulseEnabled)
.pulseColor(locationSettings.pulseColor.toArgb())
.build()
)
.locationEngineRequest(
@ -116,9 +123,9 @@ internal class MapPropertiesNode(
@Composable
internal inline fun MapUpdater(
cameraPositionState: CameraPositionState,
mapLocationSettings: MapLocationSettings,
mapUiSettings: MapUiSettings,
mapSymbolManagerSettings: MapSymbolManagerSettings,
locationSettings: MapLocationSettings,
uiSettings: MapUiSettings,
symbolManagerSettings: MapSymbolManagerSettings,
) {
val mapApplier = currentComposer.applier as MapApplier
val map = mapApplier.map
@ -132,21 +139,22 @@ internal inline fun MapUpdater(
style = style,
context = context,
cameraPositionState = cameraPositionState,
locationSettings = locationSettings,
)
},
update = {
set(mapLocationSettings.locationEnabled) { map.locationComponent.isLocationComponentEnabled = it }
set(locationSettings.locationEnabled) { map.locationComponent.isLocationComponentEnabled = it }
set(mapUiSettings.compassEnabled) { map.uiSettings.isCompassEnabled = it }
set(mapUiSettings.rotationGesturesEnabled) { map.uiSettings.isRotateGesturesEnabled = it }
set(mapUiSettings.scrollGesturesEnabled) { map.uiSettings.isScrollGesturesEnabled = it }
set(mapUiSettings.tiltGesturesEnabled) { map.uiSettings.isTiltGesturesEnabled = it }
set(mapUiSettings.zoomGesturesEnabled) { map.uiSettings.isZoomGesturesEnabled = it }
set(mapUiSettings.logoGravity) { map.uiSettings.logoGravity = it }
set(mapUiSettings.attributionGravity) { map.uiSettings.attributionGravity = it }
set(mapUiSettings.attributionTintColor) { map.uiSettings.setAttributionTintColor(it.toArgb()) }
set(uiSettings.compassEnabled) { map.uiSettings.isCompassEnabled = it }
set(uiSettings.rotationGesturesEnabled) { map.uiSettings.isRotateGesturesEnabled = it }
set(uiSettings.scrollGesturesEnabled) { map.uiSettings.isScrollGesturesEnabled = it }
set(uiSettings.tiltGesturesEnabled) { map.uiSettings.isTiltGesturesEnabled = it }
set(uiSettings.zoomGesturesEnabled) { map.uiSettings.isZoomGesturesEnabled = it }
set(uiSettings.logoGravity) { map.uiSettings.logoGravity = it }
set(uiSettings.attributionGravity) { map.uiSettings.attributionGravity = it }
set(uiSettings.attributionTintColor) { map.uiSettings.setAttributionTintColor(it.toArgb()) }
set(mapSymbolManagerSettings.iconAllowOverlap) { symbolManager.iconAllowOverlap = it }
set(symbolManagerSettings.iconAllowOverlap) { symbolManager.iconAllowOverlap = it }
update(cameraPositionState) { this.cameraPositionState = it }
}

View file

@ -124,9 +124,9 @@ public fun MapboxMap(
) {
MapUpdater(
cameraPositionState = currentCameraPositionState,
mapUiSettings = currentUiSettings,
mapLocationSettings = currentMapLocationSettings,
mapSymbolManagerSettings = currentSymbolManagerSettings,
uiSettings = currentUiSettings,
locationSettings = currentMapLocationSettings,
symbolManagerSettings = currentSymbolManagerSettings,
)
CompositionLocalProvider(
LocalCameraPositionState provides cameraPositionState,

View file

@ -22,5 +22,6 @@ sealed class AuthenticationException(message: String) : Exception(message) {
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
class SessionMissing(message: String) : AuthenticationException(message)
class Generic(message: String) : AuthenticationException(message)
class OidcError(type: String, message: String) : AuthenticationException(message)
// TODO Oidc
// class OidcError(type: String, message: String) : AuthenticationException(message)
}

View file

@ -25,7 +25,6 @@ object PermalinkBuilder {
private const val ROOM_PATH = "room/"
private const val USER_PATH = "user/"
private const val GROUP_PATH = "group/"
private val permalinkBaseUrl get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.matrixToPermalinkBaseUrl).also {
var baseUrl = it

View file

@ -63,7 +63,11 @@ interface MatrixRoom : Closeable {
val timeline: MatrixTimeline
fun open(): Result<Unit>
fun destroy()
fun subscribeToSync()
fun unsubscribeFromSync()
suspend fun userDisplayName(userId: UserId): Result<String?>
@ -133,6 +137,8 @@ interface MatrixRoom : Closeable {
zoomLevel: Int? = null,
assetType: AssetType? = null,
): Result<Unit>
override fun close() = destroy()
}

View file

@ -16,8 +16,8 @@
package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.OidcConfig
// TODO Oidc
// import io.element.android.libraries.matrix.api.auth.OidcConfig
// import org.matrix.rustcomponents.sdk.OidcClientMetadata
/*

View file

@ -17,7 +17,6 @@
package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.matrix.api.media.ImageInfo
import org.matrix.rustcomponents.sdk.MediaSource
import org.matrix.rustcomponents.sdk.ImageInfo as RustImageInfo
fun RustImageInfo.map(): ImageInfo = ImageInfo(

View file

@ -40,9 +40,6 @@ import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.backPaginationStatusFlow
import io.element.android.libraries.matrix.impl.timeline.eventOrigin
import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@ -51,11 +48,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.EventItemOrigin
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
@ -88,7 +81,6 @@ class RustMatrixRoom(
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
private val isInit = MutableStateFlow(false)
private val _syncUpdateFlow = MutableStateFlow(0L)
private val _timeline by lazy {
RustMatrixTimeline(
@ -97,6 +89,7 @@ class RustMatrixRoom(
roomCoroutineScope = roomCoroutineScope,
dispatcher = roomDispatcher,
lastLoginTimestamp = sessionData.loginTimestamp,
onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() }
)
}
@ -106,8 +99,7 @@ class RustMatrixRoom(
override val timeline: MatrixTimeline = _timeline
override fun open(): Result<Unit> {
if (isInit.value) return Result.failure(IllegalStateException("Listener already registered"))
override fun subscribeToSync() {
val settings = RoomSubscription(
requiredState = listOf(
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
@ -118,37 +110,16 @@ class RustMatrixRoom(
timelineLimit = null
)
roomListItem.subscribe(settings)
roomCoroutineScope.launch(roomDispatcher) {
innerRoom.timelineDiffFlow { initialList ->
_timeline.postItems(initialList)
}.onEach { diff ->
diff.use {
if (diff.eventOrigin() == EventItemOrigin.SYNC) {
_syncUpdateFlow.value = systemClock.epochMillis()
}
_timeline.postDiff(diff)
}
}.launchIn(this)
innerRoom.backPaginationStatusFlow()
.onEach {
_timeline.postPaginationStatus(it)
}.launchIn(this)
fetchMembers()
}
isInit.value = true
return Result.success(Unit)
}
override fun close() {
if (isInit.value) {
isInit.value = false
roomCoroutineScope.cancel()
roomListItem.unsubscribe()
innerRoom.destroy()
roomListItem.destroy()
}
override fun unsubscribeFromSync() {
roomListItem.unsubscribe()
}
override fun destroy() {
roomCoroutineScope.cancel()
innerRoom.destroy()
roomListItem.destroy()
}
override val name: String?
@ -365,12 +336,6 @@ class RustMatrixRoom(
}
}
private suspend fun fetchMembers() = withContext(roomDispatcher) {
runCatching {
innerRoom.fetchMembers()
}
}
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)

View file

@ -39,10 +39,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.BackPaginationStatus
import org.matrix.rustcomponents.sdk.EventItemOrigin
import org.matrix.rustcomponents.sdk.PaginationOptions
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.TimelineDiff
@ -59,6 +63,7 @@ class RustMatrixTimeline(
private val innerRoom: Room,
private val dispatcher: CoroutineDispatcher,
private val lastLoginTimestamp: Date?,
private val onNewSyncedEvent: () -> Unit,
) : MatrixTimeline {
private val initLatch = CompletableDeferred<Unit>()
@ -95,13 +100,40 @@ class RustMatrixTimeline(
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState.asStateFlow()
init {
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
roomCoroutineScope.launch(dispatcher) {
innerRoom.timelineDiffFlow { initialList ->
postItems(initialList)
}.onEach { diff ->
if (diff.eventOrigin() == EventItemOrigin.SYNC) {
onNewSyncedEvent()
}
postDiff(diff)
}.launchIn(this)
innerRoom.backPaginationStatusFlow()
.onEach {
postPaginationStatus(it)
}.launchIn(this)
fetchMembers()
}
}
private suspend fun fetchMembers() = withContext(dispatcher) {
runCatching {
innerRoom.fetchMembers()
}
}
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems.sample(50)
.mapLatest { items ->
encryptedHistoryPostProcessor.process(items)
}
internal suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
ensureActive()
@ -111,12 +143,12 @@ class RustMatrixTimeline(
initLatch.complete(Unit)
}
internal suspend fun postDiff(timelineDiff: TimelineDiff) {
private suspend fun postDiff(timelineDiff: TimelineDiff) {
initLatch.await()
timelineDiffProcessor.postDiff(timelineDiff)
}
internal fun postPaginationStatus(status: BackPaginationStatus) {
private fun postPaginationStatus(status: BackPaginationStatus) {
_paginationState.getAndUpdate { currentPaginationState ->
if (hasEncryptionHistoryBanner()) {
return@getAndUpdate currentPaginationState.copy(

View file

@ -45,7 +45,7 @@ class EventMessageMapper {
fun map(message: Message): MessageContent = message.use {
val type = it.msgtype().use(this::mapMessageType)
val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId)
val inReplyToEvent: InReplyTo? = (it.inReplyTo()?.event)?.use { details ->
val inReplyToEvent: InReplyTo? = it.inReplyTo()?.event?.use { details ->
when (details) {
is RepliedToEventDetails.Ready -> {
val senderProfile = details.senderProfile as? ProfileDetails.Ready

View file

@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import java.util.Date
import java.util.UUID
class TimelineEncryptedHistoryPostProcessor(
private val lastLoginTimestamp: Date?,
@ -70,5 +69,4 @@ class TimelineEncryptedHistoryPostProcessor(
val timestamp = (item as? MatrixTimelineItem.Event)?.event?.timestamp ?: return false
return timestamp <= lastLoginTimestamp!!.time
}
}

View file

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import java.util.UUID
const val A_USER_NAME = "alice"
const val A_PASSWORD = "password"

View file

@ -100,7 +100,6 @@ class FakeMatrixRoom(
private val _sentLocations = mutableListOf<SendLocationInvocation>()
val sentLocations: List<SendLocationInvocation> = _sentLocations
var invitedUserId: UserId? = null
private set
@ -128,9 +127,11 @@ class FakeMatrixRoom(
override val timeline: MatrixTimeline = matrixTimeline
override fun open(): Result<Unit> {
return Result.success(Unit)
}
override fun subscribeToSync() = Unit
override fun unsubscribeFromSync() = Unit
override fun destroy() = Unit
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
userDisplayNameResult
@ -283,8 +284,6 @@ class FakeMatrixRoom(
return sendLocationResult
}
override fun close() = Unit
fun givenLeaveRoomError(throwable: Throwable?) {
this.leaveRoomError = throwable
}

View file

@ -88,7 +88,7 @@ fun SelectedUsersList(
val maxVisibleUsers = rowWidth / userWidthWithSpacing
// Round down the number of visible users to end with a state where one is half visible
val targetFraction = (userWidth / 2) / userWidthWithSpacing
val targetFraction = userWidth / 2 / userWidthWithSpacing
val targetUsers = floor(maxVisibleUsers - targetFraction) + targetFraction
// Work out how much extra spacing we need to reduce the number of users that much, then split it evenly amongst the visible users
@ -153,7 +153,7 @@ private fun ContentToPreview() {
SelectedUsersList(
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
modifier = Modifier
.width((200 + (i * 20)).dp)
.width((200 + i * 20).dp)
.border(1.dp, Color.Red)
)
}

View file

@ -82,7 +82,6 @@ class MediaSender @Inject constructor(
progressCallback = progressCallback
)
}
else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $uploadInfo"))
}
}
}

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.mediaupload
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.vanniktech.blurhash.BlurHash
import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize
import io.element.android.libraries.androidutils.bitmap.resizeToMax
import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation
@ -92,7 +91,7 @@ class ImageCompressor @Inject constructor(
) {
val (width, height) = when (resizeMode) {
is ResizeMode.Approximate -> resizeMode.desiredWidth to resizeMode.desiredHeight
is ResizeMode.Strict -> (resizeMode.maxWidth / 2) to (resizeMode.maxHeight / 2)
is ResizeMode.Strict -> resizeMode.maxWidth / 2 to resizeMode.maxHeight / 2
is ResizeMode.None -> return
}
// Read bounds only

View file

@ -69,6 +69,7 @@ class ThumbnailFactory @Inject constructor(
cancellationSignal
)
} else {
@Suppress("DEPRECATION")
ThumbnailUtils.createImageThumbnail(
file.path,
MediaStore.Images.Thumbnails.MINI_KIND,

View file

@ -57,7 +57,5 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor)
}

View file

@ -18,7 +18,7 @@
package io.element.android.libraries.permissions.impl
import app.cash.molecule.RecompositionClock
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -41,7 +41,7 @@ class DefaultPermissionsPresenterTest {
permissionsStore,
permissionStateProvider
)
moleculeFlow(RecompositionClock.Immediate) {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
@ -64,7 +64,7 @@ class DefaultPermissionsPresenterTest {
permissionsStore,
permissionStateProvider
)
moleculeFlow(RecompositionClock.Immediate) {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
@ -84,7 +84,7 @@ class DefaultPermissionsPresenterTest {
permissionsStore,
permissionStateProvider
)
moleculeFlow(RecompositionClock.Immediate) {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
@ -113,7 +113,7 @@ class DefaultPermissionsPresenterTest {
permissionsStore,
permissionStateProvider
)
moleculeFlow(RecompositionClock.Immediate) {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
@ -142,7 +142,7 @@ class DefaultPermissionsPresenterTest {
permissionsStore,
permissionStateProvider
)
moleculeFlow(RecompositionClock.Immediate) {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
@ -164,7 +164,7 @@ class DefaultPermissionsPresenterTest {
permissionsStore,
permissionStateProvider
)
moleculeFlow(RecompositionClock.Immediate) {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()

View file

@ -16,7 +16,7 @@
package io.element.android.libraries.permissions.noop
import app.cash.molecule.RecompositionClock
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
@ -27,7 +27,7 @@ class NoopPermissionsPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = NoopPermissionsPresenter()
moleculeFlow(RecompositionClock.Immediate) {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()

View file

@ -52,9 +52,6 @@ dependencies {
implementation(projects.services.appnavstate.api)
implementation(projects.services.toolbox.api)
// TODO Temporary use the deprecated LocalBroadcastManager, to be changed later.
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
testImplementation(libs.test.junit)
testImplementation(libs.test.mockk)
testImplementation(libs.test.truth)

View file

@ -103,33 +103,6 @@ class PushersManager @Inject constructor(
return "{\"cs\":\"$secretForUser\"}"
}
suspend fun registerEmailForPush(email: String) {
TODO()
/*
val currentSession = activeSessionHolder.getActiveSession()
val appName = appNameProvider.getAppName()
currentSession.pushersService().addEmailPusher(
email = email,
lang = localeProvider.current().language,
emailBranding = appName,
appDisplayName = appName,
deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE"
)
*/
}
fun getPusherForCurrentSession() {}/*: Pusher? {
val session = activeSessionHolder.getSafeActiveSession() ?: return null
val deviceId = session.sessionParams.deviceId
return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
}
*/
suspend fun unregisterEmailPusher(email: String) {
// val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
// currentSession.pushersService().removeEmailPusher(email)
}
override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
matrixClient.pushersService().unsetHttpPusher()
}

View file

@ -23,17 +23,17 @@ import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.api.store.PushDataStore
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.services.appnavstate.api.NavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState
import io.element.android.services.appnavstate.api.currentSessionId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -48,7 +48,6 @@ import javax.inject.Inject
*/
@SingleIn(AppScope::class)
class DefaultNotificationDrawerManager @Inject constructor(
private val pushDataStore: PushDataStore,
private val notifiableEventProcessor: NotifiableEventProcessor,
private val notificationRenderer: NotificationRenderer,
private val notificationEventPersistence: NotificationEventPersistence,
@ -76,9 +75,16 @@ class DefaultNotificationDrawerManager @Inject constructor(
}
}
private var currentAppNavigationState: NavigationState? = null
private fun onAppNavigationStateChange(navigationState: NavigationState) {
when (navigationState) {
NavigationState.Root -> {}
NavigationState.Root -> {
currentAppNavigationState?.currentSessionId()?.let { sessionId ->
// User signed out, clear all notifications related to the session.
clearAllEvents(sessionId)
}
}
is NavigationState.Session -> {}
is NavigationState.Space -> {}
is NavigationState.Room -> {
@ -93,6 +99,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
)
}
}
currentAppNavigationState = navigationState
}
private fun createInitialNotificationState(): NotificationState {
@ -133,12 +140,21 @@ class DefaultNotificationDrawerManager @Inject constructor(
/**
* Clear all known events and refresh the notification drawer.
*/
fun clearAllEvents(sessionId: SessionId) {
fun clearAllMessagesEvents(sessionId: SessionId) {
updateEvents {
it.clearMessagesForSession(sessionId)
}
}
/**
* Clear all notifications related to the session and refresh the notification drawer.
*/
fun clearAllEvents(sessionId: SessionId) {
updateEvents {
it.clearAllForSession(sessionId)
}
}
/**
* Should be called when the application is currently opened and showing timeline for the given roomId.
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.

View file

@ -27,7 +27,7 @@ class FilteredEventDetector @Inject constructor(
* Returns true if the given event should be ignored.
* Used to skip notifications if a non expected message is received.
*/
fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean {
fun shouldBeIgnored(@Suppress("UNUSED_PARAMETER") notifiableEvent: NotifiableEvent): Boolean {
/* TODO EAx
val session = activeSessionDataSource.currentValue?.orNull() ?: return false

View file

@ -17,7 +17,7 @@
package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
@ -34,7 +34,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
@ -57,9 +56,6 @@ private val loggerTag = LoggerTag("NotifiableEventResolver", pushLoggerTag)
*/
class NotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider,
// private val noticeEventFormatter: NoticeEventFormatter,
// private val displayableEventFormatter: DisplayableEventFormatter,
private val buildMeta: BuildMeta,
private val clock: SystemClock,
private val matrixClientProvider: MatrixClientProvider,
) {

View file

@ -19,15 +19,12 @@ package io.element.android.libraries.push.impl.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.RemoteInput
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.push.impl.log.notificationLoggerTag
import io.element.android.services.toolbox.api.systemclock.SystemClock
import timber.log.Timber
import javax.inject.Inject
@ -39,10 +36,6 @@ private val loggerTag = LoggerTag("NotificationBroadcastReceiver", notificationL
class NotificationBroadcastReceiver : BroadcastReceiver() {
@Inject lateinit var defaultNotificationDrawerManager: DefaultNotificationDrawerManager
//@Inject lateinit var activeSessionHolder: ActiveSessionHolder
//@Inject lateinit var analyticsTracker: AnalyticsTracker
@Inject lateinit var clock: SystemClock
@Inject lateinit var actionIds: NotificationActionIds
override fun onReceive(context: Context?, intent: Intent?) {
@ -59,7 +52,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
}
actionIds.dismissSummary ->
defaultNotificationDrawerManager.clearAllEvents(sessionId)
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId)
actionIds.dismissInvite -> if (roomId != null) {
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
}
@ -81,6 +74,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
}
}
@Suppress("UNUSED_PARAMETER")
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) {
/*
activeSessionHolder.getSafeActiveSession()?.let { session ->
@ -94,10 +88,10 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
}
}
}
*/
}
@Suppress("UNUSED_PARAMETER")
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) {
/*
activeSessionHolder.getSafeActiveSession()?.let { session ->
@ -109,6 +103,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
*/
}
@Suppress("UNUSED_PARAMETER")
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) {
/*
activeSessionHolder.getActiveSession().let { session ->
@ -123,7 +118,9 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
*/
}
@Suppress("UNUSED_PARAMETER")
private fun handleSmartReply(intent: Intent, context: Context) {
/*
val message = getReplyMessage(intent)
val sessionId = intent.getStringExtra(KEY_SESSION_ID)?.let(::SessionId)
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
@ -134,13 +131,11 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
// Can this happen? should we update notification?
return
}
/*
activeSessionHolder.getActiveSession().let { session ->
session.getRoom(roomId)?.let { room ->
sendMatrixEvent(message, threadId, session, room, context)
}
}
*/
}
@ -234,6 +229,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
*/
/*
private fun getReplyMessage(intent: Intent?): String? {
if (intent != null) {
val remoteInput = RemoteInput.getResultsFromIntent(intent)
@ -243,6 +239,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
}
return null
}
*/
companion object {
const val KEY_SESSION_ID = "sessionID"

View file

@ -154,6 +154,11 @@ data class NotificationEventQueue constructor(
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId }
}
fun clearAllForSession(sessionId: SessionId) {
Timber.d("clearAllForSession $sessionId")
queue.removeAll { it.sessionId == sessionId }
}
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
Timber.d("clearMessageEventOfRoom $sessionId, $roomId")
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId }

View file

@ -28,7 +28,7 @@ class OutdatedEventDetector @Inject constructor(
* Used to clean up notifications if a displayed message has been read on an
* other device.
*/
fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean {
fun isMessageOutdated(@Suppress("UNUSED_PARAMETER") notifiableEvent: NotifiableEvent): Boolean {
/* TODO EAx
val session = activeSessionDataSource.currentValue?.orNull() ?: return false

View file

@ -69,7 +69,7 @@ class RoomGroupMessageCreator @Inject constructor(
val lastMessageTimestamp = events.last().timestamp
val smartReplyErrors = events.filter { it.isSmartReplyError() }
val messageCount = (events.size - smartReplyErrors.size)
val messageCount = events.size - smartReplyErrors.size
val meta = RoomNotification.Message.Meta(
summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, roomIsDirect = !roomIsGroup),
messageCount = messageCount,

View file

@ -19,12 +19,10 @@ package io.element.android.libraries.push.impl.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
class TestNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Internal broadcast to any one interested
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
// TODO The test notification has been clicked, notify the ui
}
}

View file

@ -162,7 +162,7 @@ class NotificationChannels @Inject constructor(
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
private fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
fun openSystemSettingsForSilentCategory(activity: Activity) {
activity.startNotificationChannelSettingsIntent(SILENT_NOTIFICATION_CHANNEL_ID)

View file

@ -16,22 +16,17 @@
package io.element.android.libraries.push.impl.push
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.push.impl.PushersManager
import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler
@ -53,8 +48,7 @@ class DefaultPushHandler @Inject constructor(
private val defaultPushDataStore: DefaultPushDataStore,
private val userPushStoreFactory: UserPushStoreFactory,
private val pushClientSecret: PushClientSecret,
private val actionIds: NotificationActionIds,
@ApplicationContext private val context: Context,
// private val actionIds: NotificationActionIds,
private val buildMeta: BuildMeta,
private val matrixAuthenticationService: MatrixAuthenticationService,
) : PushHandler {
@ -82,8 +76,8 @@ class DefaultPushHandler @Inject constructor(
// Diagnostic Push
if (pushData.eventId == PushersManager.TEST_EVENT_ID) {
val intent = Intent(actionIds.push)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
// val intent = Intent(actionIds.push)
// TODO The test push has been received, notify the ui
return
}

View file

@ -1,45 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.pushproviders.firebase
import javax.inject.Inject
// TODO
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
// private val unifiedPushHelper: UnifiedPushHelper,
// private val fcmHelper: FcmHelper,
// private val activeSessionHolder: ActiveSessionHolder,
) {
// fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
// if (unifiedPushHelper.isEmbeddedDistributor()) {
// fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
// }
// }
private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
/*
TODO EAx
val currentSession = activeSessionHolder.getActiveSession()
val currentPushers = currentSession.pushersService().getPushers()
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
*/
true
} else {
false
}
}

View file

@ -26,7 +26,7 @@ class UnregisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context,
//private val pushDataStore: PushDataStore,
private val unifiedPushStore: UnifiedPushStore,
private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver,
// private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver,
) {
suspend fun execute(clientSecret: String /*pushersManager: PushersManager?*/) {

View file

@ -20,6 +20,11 @@ plugins {
android {
namespace = "io.element.android.libraries.push.pushstore.impl"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
}
anvil {
@ -43,4 +48,13 @@ dependencies {
testImplementation(libs.coroutines.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.appnavstate.test)
androidTestImplementation(libs.coroutines.test)
androidTestImplementation(libs.test.core)
androidTestImplementation(libs.test.junit)
androidTestImplementation(libs.test.truth)
androidTestImplementation(libs.test.runner)
androidTestImplementation(projects.libraries.sessionStorage.test)
coreLibraryDesugaring(libs.android.desugar)
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.pushstore.impl
import androidx.test.platform.app.InstrumentationRegistry
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
import kotlinx.coroutines.runBlocking
import org.junit.Test
import kotlin.concurrent.thread
/**
* Note: to clear the emulator, invoke:
* adb uninstall io.element.android.libraries.push.pushstore.impl.test
*/
class DefaultUserPushStoreFactoryTest {
/**
* Ensure that creating UserPushStore is thread safe.
*/
@Test
fun testParallelCreation() {
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
val sessionId = SessionId("@alice:server.org")
val userPushStoreFactory = DefaultUserPushStoreFactory(context, NoOpSessionObserver())
var userPushStore1: UserPushStore? = null
val thread1 = thread {
userPushStore1 = userPushStoreFactory.create(sessionId)
}
var userPushStore2: UserPushStore? = null
val thread2 = thread {
userPushStore2 = userPushStoreFactory.create(sessionId)
}
thread1.join()
thread2.join()
runBlocking {
userPushStore1!!.areNotificationEnabledForDevice()
userPushStore2!!.areNotificationEnabledForDevice()
}
}
}

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@SingleIn(AppScope::class)
@ -39,7 +40,7 @@ class DefaultUserPushStoreFactory @Inject constructor(
}
// We can have only one class accessing a single data store, so keep a cache of them.
private val cache = mutableMapOf<SessionId, UserPushStore>()
private val cache = ConcurrentHashMap<SessionId, UserPushStore>()
override fun create(userId: SessionId): UserPushStore {
return cache.getOrPut(userId) {
UserPushStoreDataStore(

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.sessionstorage.test"
}
dependencies {
implementation(projects.libraries.sessionStorage.api)
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.sessionstorage.test.observer
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
class NoOpSessionObserver : SessionObserver {
override fun addListener(listener: SessionListener) = Unit
override fun removeListener(listener: SessionListener) = Unit
}

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="rich_text_editor_a11y_add_attachment">"Pridať prílohu"</string>
<string name="rich_text_editor_bullet_list">"Prepnúť zoznam odrážok"</string>
<string name="rich_text_editor_code_block">"Prepnúť blok kódu"</string>
<string name="rich_text_editor_composer_placeholder">"Správa…"</string>

View file

@ -143,6 +143,8 @@
<string name="error_failed_loading_map">"%1$s nedokázal načítať mapu. Skúste to prosím neskôr."</string>
<string name="error_failed_loading_messages">"Načítanie správ zlyhalo"</string>
<string name="error_failed_locating_user">"%1$s nemohol získať prístup k vašej polohe. Skúste to prosím neskôr."</string>
<string name="error_missing_location_auth_android">"Ak chcete odoslať polohu, povoľte %1$s prístup k vašej polohe z obrazovky nastavení."</string>
<string name="error_missing_location_rationale_android">"Ak chcete odoslať polohu, povoľte %1$s prístup k vašej polohe v nasledujúcom dialógovom okne."</string>
<string name="error_some_messages_have_not_been_sent">"Niektoré správy neboli odoslané"</string>
<string name="error_unknown">"Prepáčte, vyskytla sa chyba"</string>
<string name="invite_friends_rich_title">"🔐️ Pripojte sa ku mne na %1$s"</string>

View file

@ -143,8 +143,8 @@
<string name="error_failed_loading_map">"%1$s could not load the map. Please try again later."</string>
<string name="error_failed_loading_messages">"Failed loading messages"</string>
<string name="error_failed_locating_user">"%1$s could not access your location. Please try again later."</string>
<string name="error_missing_location_auth_android">"To send a location, allow %1$s to access your location from its settings screen."</string>
<string name="error_missing_location_rationale_android">"To send a location, allow %1$s to access your location in the next dialog."</string>
<string name="error_missing_location_auth_android">"%1$s does not have permission to access your location. You can enable access in Settings."</string>
<string name="error_missing_location_rationale_android">"%1$s does not have permission to access your location. Enable access below."</string>
<string name="error_some_messages_have_not_been_sent">"Some messages have not been sent"</string>
<string name="error_unknown">"Sorry, an error occurred"</string>
<string name="invite_friends_rich_title">"🔐️ Join me on %1$s"</string>